chore: checkpoint team runtime work

This commit is contained in:
777genius 2026-05-06 11:53:34 +03:00
parent dbd9ca7cf7
commit d60abd54fe
26 changed files with 3146 additions and 124 deletions

View file

@ -911,14 +911,17 @@ async function memberBriefing(context, memberName, options = {}) {
config.projectPath.trim() :
'';
const activeProcesses = processStore
.listProcesses(context.paths)
.filter(
(entry) =>
entry &&
entry.alive &&
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
);
const includeActiveProcesses = options.includeActiveProcesses !== false;
const activeProcesses = includeActiveProcesses ?
processStore
.listProcesses(context.paths)
.filter(
(entry) =>
entry &&
entry.alive &&
normalizeMemberName(entry.registeredBy) === normalizeMemberName(requestedMemberName)
) :
[];
const taskQueue = await taskBriefing(context, requestedMemberName);
const completionNotifyExample = messagingProtocol.buildLeadMessageExample({

View file

@ -43,7 +43,7 @@ declare module 'agent-teams-controller' {
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
memberBriefing(
memberName: string,
options?: { runtimeProvider?: 'native' | 'opencode' }
options?: { runtimeProvider?: 'native' | 'opencode'; includeActiveProcesses?: boolean }
): Promise<string>;
leadBriefing(): Promise<string>;
taskBriefing(memberName: string): Promise<string>;

View file

@ -622,8 +622,9 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
...toolContextSchema,
memberName: z.string().min(1),
runtimeProvider: z.enum(['native', 'opencode']).optional(),
includeActiveProcesses: z.boolean().optional(),
}),
execute: async ({ teamName, claudeDir, memberName, runtimeProvider }) => {
execute: async ({ teamName, claudeDir, memberName, runtimeProvider, includeActiveProcesses }) => {
assertConfiguredTeam(teamName, claudeDir);
return {
content: [
@ -631,6 +632,7 @@ export function registerTaskTools(server: Pick<FastMCP, 'addTool'>) {
type: 'text' as const,
text: await getController(teamName, claudeDir).tasks.memberBriefing(memberName, {
...(runtimeProvider ? { runtimeProvider } : {}),
...(includeActiveProcesses !== undefined ? { includeActiveProcesses } : {}),
}),
},
],

View file

@ -22,7 +22,11 @@
"dev:web": "node ./scripts/dev-web.mjs",
"dev:kill": "node bin/kill-dev.js",
"opencode:prove-mixed-recovery": "node ./scripts/prove-opencode-mixed-recovery.mjs",
"opencode:prove-semantic-gauntlet": "node ./scripts/prove-opencode-semantic-gauntlet.mjs",
"opencode:prove-semantic-messaging": "node ./scripts/prove-opencode-semantic-messaging.mjs",
"opencode:prove-semantic-model-matrix": "node ./scripts/prove-opencode-semantic-model-matrix.mjs",
"opencode:prove-team-provisioning": "node ./scripts/prove-opencode-team-provisioning.mjs",
"team:prove-agent-cli-launch": "node ./scripts/prove-agent-cli-launch.mjs",
"team:prove-launch-matrix": "pnpm exec vitest run --maxWorkers 1 --minWorkers 1 test/main/services/team/TeamAgentLaunchMatrix.safe-e2e.test.ts",
"prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
"build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build",

View file

@ -0,0 +1,43 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
const env = {
...process.env,
AGENT_CLI_LAUNCH_LIVE_E2E: '1',
};
console.log('Running agent CLI launch live smoke');
const result = spawnSync(
'pnpm',
[
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'test/main/utils/AgentCliLaunch.live-e2e.test.ts',
],
{
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);
if (result.error) {
console.error(`Failed to run agent CLI launch smoke: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);

View file

@ -0,0 +1,73 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
const env = {
...process.env,
OPENCODE_E2E: '1',
OPENCODE_E2E_SEMANTIC_MODEL_GAUNTLET: '1',
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
OPENCODE_E2E_GAUNTLET_RUNS: process.env.OPENCODE_E2E_GAUNTLET_RUNS?.trim() || '1',
OPENCODE_E2E_GAUNTLET_MIN_AVERAGE_SCORE:
process.env.OPENCODE_E2E_GAUNTLET_MIN_AVERAGE_SCORE?.trim() || '80',
OPENCODE_E2E_GAUNTLET_MIN_SUCCESSFUL_RUNS:
process.env.OPENCODE_E2E_GAUNTLET_MIN_SUCCESSFUL_RUNS?.trim() || '1',
OPENCODE_E2E_GAUNTLET_MIN_CONSISTENCY_SCORE:
process.env.OPENCODE_E2E_GAUNTLET_MIN_CONSISTENCY_SCORE?.trim() || '0',
OPENCODE_E2E_GAUNTLET_REQUIRE_RECOMMENDED:
process.env.OPENCODE_E2E_GAUNTLET_REQUIRE_RECOMMENDED?.trim() || '1',
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
};
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
}
console.log('Running OpenCode semantic gauntlet live smoke');
console.log(`Models: ${env.OPENCODE_E2E_MODELS?.trim() || env.OPENCODE_E2E_MODEL}`);
console.log(`Runs per model: ${env.OPENCODE_E2E_GAUNTLET_RUNS}`);
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight);
const result = spawnSync(
'pnpm',
[
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'test/main/services/team/OpenCodeSemanticModelGauntlet.live.test.ts',
],
{
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);
if (result.error) {
console.error(`Failed to run OpenCode semantic gauntlet smoke: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);

View file

@ -0,0 +1,65 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
const env = {
...process.env,
OPENCODE_E2E: '1',
OPENCODE_E2E_SEMANTIC_MESSAGING: '1',
OPENCODE_E2E_PROJECT_PATH: process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || repoRoot,
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
};
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
}
console.log('Running OpenCode semantic messaging live smoke');
console.log(`Model: ${env.OPENCODE_E2E_MODEL}`);
console.log(`Project: ${env.OPENCODE_E2E_PROJECT_PATH}`);
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight);
const result = spawnSync(
'pnpm',
[
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'test/main/services/team/OpenCodeSemanticMessaging.live.test.ts',
],
{
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);
if (result.error) {
console.error(`Failed to run OpenCode semantic messaging smoke: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);

View file

@ -0,0 +1,63 @@
#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import {
exitForSkippedPreflight,
preflightOpenCodeLiveEnvironment,
} from './lib/opencode-live-preflight.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(scriptDir, '..');
const orchestratorRoot = process.env.CLAUDE_DEV_RUNTIME_ROOT?.trim();
const siblingOrchestrator = path.resolve(repoRoot, '..', 'agent_teams_orchestrator');
const env = {
...process.env,
OPENCODE_E2E: '1',
OPENCODE_E2E_SEMANTIC_MODEL_MATRIX: '1',
OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle',
OPENCODE_DISABLE_AUTOUPDATE: process.env.OPENCODE_DISABLE_AUTOUPDATE ?? '1',
};
if (!env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim()) {
const runtimeRoot = orchestratorRoot ? path.resolve(orchestratorRoot) : siblingOrchestrator;
env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH = path.join(runtimeRoot, 'cli');
}
console.log('Running OpenCode semantic model matrix live smoke');
console.log(`Models: ${env.OPENCODE_E2E_MODELS?.trim() || env.OPENCODE_E2E_MODEL}`);
console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`);
const preflight = await preflightOpenCodeLiveEnvironment({ repoRoot });
exitForSkippedPreflight(preflight);
const result = spawnSync(
'pnpm',
[
'exec',
'vitest',
'run',
'--maxWorkers',
'1',
'--minWorkers',
'1',
'test/main/services/team/OpenCodeSemanticModelMatrix.live.test.ts',
],
{
cwd: repoRoot,
env,
stdio: 'inherit',
shell: process.platform === 'win32',
}
);
if (result.error) {
console.error(`Failed to run OpenCode semantic model matrix smoke: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);

View file

@ -6,6 +6,9 @@ import type {
MemberLaunchState,
MemberSpawnLivenessSource,
MemberSpawnStatusEntry,
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
OpenCodeBootstrapMode,
PersistedTeamLaunchMemberSources,
PersistedTeamLaunchMemberState,
PersistedTeamLaunchPhase,
@ -43,6 +46,9 @@ export interface MixedSecondaryLaneMemberStateInput {
runtimePid?: number;
runtimeSessionId?: string;
sessionId?: string;
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
bootstrapMode?: OpenCodeBootstrapMode;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
livenessKind?: TeamAgentRuntimeLivenessKind;
pidSource?: TeamAgentRuntimePidSource;
runtimeDiagnostic?: string;
@ -348,6 +354,9 @@ function createSecondaryLaneMemberState(
? Math.trunc(evidence.runtimePid)
: undefined,
runtimeSessionId: evidence?.runtimeSessionId ?? evidence?.sessionId,
bootstrapEvidenceSource: evidence?.bootstrapEvidenceSource,
bootstrapMode: evidence?.bootstrapMode,
appManagedBootstrapCandidate: evidence?.appManagedBootstrapCandidate,
livenessKind: evidence?.livenessKind,
pidSource: evidence?.pidSource,
runtimeDiagnostic: evidence?.runtimeDiagnostic,

View file

@ -6,6 +6,7 @@ import type {
MemberLaunchState,
MemberSpawnLivenessSource,
MemberSpawnStatusEntry,
OpenCodeAppManagedBootstrapCandidate,
PersistedTeamLaunchMemberSources,
PersistedTeamLaunchMemberState,
PersistedTeamLaunchPhase,
@ -176,6 +177,60 @@ function normalizeOptionalString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
}
function normalizeOpenCodeAppManagedBootstrapCandidate(
value: unknown
): OpenCodeAppManagedBootstrapCandidate | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
const record = value as Record<string, unknown>;
if (record.schemaVersion !== 1 || record.source !== 'app_managed_bootstrap') {
return undefined;
}
const teamName = normalizeOptionalString(record.teamName);
const memberName = normalizeOptionalString(record.memberName);
const runId = normalizeOptionalString(record.runId);
const laneId = normalizeOptionalString(record.laneId);
const runtimeSessionId = normalizeOptionalString(record.runtimeSessionId);
const messageID = normalizeOptionalString(record.messageID);
const contextHash = normalizeOptionalString(record.contextHash);
const briefingHash = normalizeOptionalString(record.briefingHash);
const injectionVerifiedAt = normalizeOptionalString(record.injectionVerifiedAt);
const candidateAt = normalizeOptionalString(record.candidateAt);
if (
!teamName ||
!memberName ||
!runId ||
!laneId ||
!runtimeSessionId ||
!messageID ||
!contextHash ||
!briefingHash ||
!injectionVerifiedAt ||
!candidateAt
) {
return undefined;
}
const model = normalizeOptionalString(record.model);
const agent = normalizeOptionalString(record.agent);
return {
schemaVersion: 1,
source: 'app_managed_bootstrap',
teamName,
memberName,
runId,
laneId,
runtimeSessionId,
messageID,
contextHash,
briefingHash,
injectionVerifiedAt,
candidateAt,
...(model ? { model } : {}),
...(agent ? { agent } : {}),
};
}
function decodeJsonStringLiteral(value: string): string {
try {
return JSON.parse(`"${value}"`) as string;
@ -601,6 +656,19 @@ function normalizePersistedMemberState(
runtimePid: normalizeRuntimePid(parsed.runtimePid),
runtimeRunId: normalizeOptionalString(parsed.runtimeRunId),
runtimeSessionId: normalizeOptionalString(parsed.runtimeSessionId),
bootstrapEvidenceSource:
parsed.bootstrapEvidenceSource === 'runtime_bootstrap_checkin' ||
parsed.bootstrapEvidenceSource === 'app_managed_bootstrap'
? parsed.bootstrapEvidenceSource
: undefined,
bootstrapMode:
parsed.bootstrapMode === 'model_tool_checkin' ||
parsed.bootstrapMode === 'app_managed_context'
? parsed.bootstrapMode
: undefined,
appManagedBootstrapCandidate: normalizeOpenCodeAppManagedBootstrapCandidate(
parsed.appManagedBootstrapCandidate
),
livenessKind,
pidSource: normalizePidSource(parsed.pidSource),
runtimeDiagnostic: normalizeOptionalString(parsed.runtimeDiagnostic),

View file

@ -402,6 +402,8 @@ import type {
MemberSpawnStatus,
MemberSpawnStatusEntry,
MemberSpawnStatusesSnapshot,
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
OpenCodeRuntimeDeliveryStatus,
PersistedTeamLaunchMemberState,
PersistedTeamLaunchPhase,
@ -2160,6 +2162,28 @@ function downgradeUncommittedOpenCodeBootstrapEvidence(
};
}
function promoteCommittedOpenCodeAppManagedBootstrapEvidence(
evidence: TeamRuntimeMemberLaunchEvidence
): TeamRuntimeMemberLaunchEvidence {
return {
...evidence,
launchState: 'confirmed_alive',
agentToolAccepted: true,
runtimeAlive: true,
bootstrapConfirmed: true,
hardFailure: false,
hardFailureReason: undefined,
livenessKind: 'confirmed_bootstrap',
runtimeDiagnostic:
'OpenCode app-managed bootstrap evidence was committed and read back by the desktop app.',
runtimeDiagnosticSeverity: 'info',
diagnostics: appendDiagnosticOnce(
evidence.diagnostics,
'OpenCode app-managed bootstrap evidence committed and read back.'
),
};
}
function summarizeRuntimeLaunchResultMembers(
members: Record<string, TeamRuntimeMemberLaunchEvidence>
): TeamLaunchAggregateState {
@ -2493,6 +2517,7 @@ const OPEN_CODE_SECRET_FLAG_PATTERN =
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
const OPEN_CODE_BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Z0-9._~+/=-]+/gi;
const OPEN_CODE_SECRET_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{16,}\b/g;
const OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS = 12_000;
function normalizeOpenCodePersistedFailureReason(value: string | undefined): string | undefined {
const trimmed = value?.replace(/\s+/g, ' ').trim();
@ -2505,6 +2530,21 @@ function normalizeOpenCodePersistedFailureReason(value: string | undefined): str
.replace(OPEN_CODE_SECRET_KEY_PATTERN, '[redacted-api-key]');
}
function redactOpenCodeAppManagedContextText(value: string): string {
return value
.replace(OPEN_CODE_SECRET_FLAG_PATTERN, '$1[redacted]')
.replace(OPEN_CODE_BEARER_TOKEN_PATTERN, 'Bearer [redacted]')
.replace(OPEN_CODE_SECRET_KEY_PATTERN, '[redacted-api-key]');
}
function boundOpenCodeAppManagedBriefingText(value: string): string {
const normalized = redactOpenCodeAppManagedContextText(value.replace(/\r\n/g, '\n')).trim();
if (normalized.length <= OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS) {
return normalized;
}
return `${normalized.slice(0, OPEN_CODE_APP_MANAGED_BRIEFING_MAX_CHARS)}\n[truncated app-managed briefing]`;
}
function isGenericOpenCodePersistedFailureReason(value: string | undefined): boolean {
const normalized = normalizeOpenCodePersistedFailureReason(value);
return (
@ -2616,8 +2656,20 @@ function promoteOpenCodeSecondaryMemberFromCommittedBootstrapEvidence(input: {
hardFailureReason: undefined,
runtimeRunId: input.session.runId ?? input.current.runtimeRunId,
runtimeSessionId: input.session.id,
bootstrapEvidenceSource: input.session.source,
bootstrapMode:
input.session.source === 'app_managed_bootstrap'
? 'app_managed_context'
: 'model_tool_checkin',
appManagedBootstrapCandidate:
input.session.source === 'app_managed_bootstrap'
? input.session.appManagedBootstrapCandidate
: undefined,
livenessKind,
runtimeDiagnostic: 'OpenCode bootstrap evidence committed.',
runtimeDiagnostic:
input.session.source === 'app_managed_bootstrap'
? 'OpenCode app-managed bootstrap evidence committed.'
: 'OpenCode bootstrap evidence committed.',
runtimeDiagnosticSeverity: 'info',
firstSpawnAcceptedAt:
input.current.firstSpawnAcceptedAt ?? input.previous?.firstSpawnAcceptedAt ?? observedAt,
@ -6687,6 +6739,13 @@ export class TeamProvisioningService {
};
}
private isOpenCodePromptAcceptedByObservation(
observation?: NonNullable<OpenCodeTeamRuntimeMessageResult['responseObservation']>
): boolean {
const deliveredUserMessageId = observation?.deliveredUserMessageId;
return typeof deliveredUserMessageId === 'string' && deliveredUserMessageId.trim().length > 0;
}
private isOpenCodeDeliveryRetryablePendingResponse(input: {
ledgerRecord: OpenCodePromptDeliveryLedgerRecord;
visibleReply?: OpenCodeVisibleReplyProof | null;
@ -7115,6 +7174,42 @@ export class TeamProvisioningService {
}
}
private async isStaleOpenCodePromptDeliveryWatchdogError(input: {
teamName: string;
memberName: string;
messageId: string;
error: unknown;
}): Promise<boolean> {
if (!getErrorMessage(input.error).startsWith('OpenCode prompt delivery record not found:')) {
return false;
}
if (!this.canDeliverToOpenCodeRuntimeForTeam(input.teamName)) {
return true;
}
const inboxMessages = await this.inboxReader
.getMessagesFor(input.teamName, input.memberName)
.catch(() => []);
const targetMessage = inboxMessages.find((message) => message.messageId === input.messageId);
if (!targetMessage || targetMessage.read) {
return true;
}
const identity = await this.resolveOpenCodeMemberDeliveryIdentity(
input.teamName,
input.memberName
).catch(() => null);
if (!identity?.ok) {
return true;
}
const laneActive = await this.isOpenCodeRuntimeLaneIndexActive(
input.teamName,
identity.laneId
).catch(() => false);
return !laneActive;
}
private scheduleOpenCodePromptDeliveryWatchdog(input: {
teamName: string;
memberName: string;
@ -7141,10 +7236,30 @@ export class TeamProvisioningService {
this.enqueueOpenCodePromptDeliveryWatchdogJob({
teamName: input.teamName,
run: async () => {
await this.relayOpenCodeMemberInboxMessages(input.teamName, input.memberName, {
onlyMessageId: messageId,
source: 'watchdog',
});
if (!this.canDeliverToOpenCodeRuntimeForTeam(input.teamName)) {
return;
}
try {
await this.relayOpenCodeMemberInboxMessages(input.teamName, input.memberName, {
onlyMessageId: messageId,
source: 'watchdog',
});
} catch (error) {
if (
await this.isStaleOpenCodePromptDeliveryWatchdogError({
teamName: input.teamName,
memberName: input.memberName,
messageId,
error,
})
) {
logger.debug(
`[${input.teamName}] Ignoring stale OpenCode prompt delivery watchdog job for ${input.memberName}/${messageId}: ${getErrorMessage(error)}`
);
return;
}
throw error;
}
},
});
}, delayMs);
@ -8098,16 +8213,18 @@ export class TeamProvisioningService {
const responseObservation = this.normalizeOpenCodeDeliveryResponseObservation(
result.responseObservation
);
const promptAccepted =
result.ok || this.isOpenCodePromptAcceptedByObservation(responseObservation);
if (ledgerRecord && ledger) {
ledgerRecord = await ledger.applyDeliveryResult({
id: ledgerRecord.id,
accepted: result.ok,
accepted: promptAccepted,
attempted: true,
responseObservation,
sessionId: result.sessionId,
prePromptCursor: result.prePromptCursor,
diagnostics: result.diagnostics,
reason: result.ok ? responseObservation?.reason : result.diagnostics[0],
reason: promptAccepted ? responseObservation?.reason : result.diagnostics[0],
now: nowIso(),
});
let proof = await this.applyOpenCodeVisibleDestinationProof({
@ -8127,7 +8244,7 @@ export class TeamProvisioningService {
});
ledgerRecord = proof.ledgerRecord;
this.logOpenCodePromptDeliveryEvent(
result.ok
promptAccepted
? ledgerRecord.status === 'unanswered'
? 'opencode_prompt_delivery_unanswered'
: ledgerRecord.status === 'responded'
@ -8135,7 +8252,10 @@ export class TeamProvisioningService {
: 'opencode_prompt_delivery_prompt_accepted'
: 'opencode_prompt_delivery_retry_scheduled',
ledgerRecord,
{ accepted: result.ok, reason: ledgerRecord.lastReason ?? result.diagnostics[0] ?? null }
{
accepted: promptAccepted,
reason: ledgerRecord.lastReason ?? result.diagnostics[0] ?? null,
}
);
}
const responseState = ledgerRecord?.responseState ?? responseObservation?.state;
@ -8160,7 +8280,7 @@ export class TeamProvisioningService {
visibleReply,
ledgerRecord,
});
if (ledgerRecord && result.ok && !readAllowed) {
if (ledgerRecord && promptAccepted && !readAllowed) {
const retry = this.isOpenCodeDeliveryRetryablePendingResponse({
ledgerRecord,
visibleReply,
@ -8196,7 +8316,7 @@ export class TeamProvisioningService {
};
}
}
if (ledgerRecord && !result.ok) {
if (ledgerRecord && !promptAccepted) {
const reason = this.isOpenCodePromptAcceptanceUnknownFailure(result.diagnostics)
? 'opencode_prompt_acceptance_unknown_after_bridge_timeout'
: (result.diagnostics[0] ?? 'opencode_message_delivery_failed');
@ -8239,9 +8359,9 @@ export class TeamProvisioningService {
ledgerRecord?.visibleReplyCorrelation ??
responseObservation?.visibleReplyCorrelation ??
undefined;
const acceptanceUnknown = Boolean(ledgerRecord?.acceptanceUnknown && !result.ok);
const acceptanceUnknown = Boolean(ledgerRecord?.acceptanceUnknown && !promptAccepted);
const responsePending =
acceptanceUnknown || (result.ok && Boolean(ledgerRecord || responseObservation))
acceptanceUnknown || (promptAccepted && Boolean(ledgerRecord || responseObservation))
? !readAllowed
: false;
const pendingReason =
@ -8255,8 +8375,8 @@ export class TeamProvisioningService {
? ledgerRecord.diagnostics
: result.diagnostics;
return {
delivered: result.ok || acceptanceUnknown,
...(ledgerRecord || responseObservation ? { accepted: result.ok } : {}),
delivered: promptAccepted || acceptanceUnknown,
...(ledgerRecord || responseObservation ? { accepted: promptAccepted } : {}),
...(ledgerRecord || responseObservation ? { responsePending } : {}),
...(acceptanceUnknown ? { acceptanceUnknown: true } : {}),
...(ledgerRecord
@ -8279,7 +8399,7 @@ export class TeamProvisioningService {
: {}),
...(pendingReason
? { reason: pendingReason }
: result.ok
: promptAccepted
? {}
: { reason: result.diagnostics[0] ?? 'opencode_message_delivery_failed' }),
diagnostics,
@ -9927,6 +10047,8 @@ export class TeamProvisioningService {
memberName: string;
runtimeSessionId: string;
observedAt: string;
source?: OpenCodeBootstrapEvidenceSource;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
}): Promise<void> {
const descriptor = OPENCODE_RUNTIME_STORE_DESCRIPTORS.find(
(candidate) => candidate.schemaName === 'opencode.sessionStore'
@ -9944,6 +10066,7 @@ export class TeamProvisioningService {
await fs.promises.mkdir(runtimeDirectory, { recursive: true });
const sessionStorePath = path.join(runtimeDirectory, descriptor.relativePath);
const existingSessions = await this.readOpenCodeRuntimeSessionStore(sessionStorePath);
const source = input.source ?? 'runtime_bootstrap_checkin';
const session = {
id: input.runtimeSessionId,
teamName: input.teamName,
@ -9952,7 +10075,10 @@ export class TeamProvisioningService {
laneId: input.laneId,
providerId: 'opencode',
observedAt: input.observedAt,
source: 'runtime_bootstrap_checkin',
source,
...(source === 'app_managed_bootstrap' && input.appManagedBootstrapCandidate
? { appManagedBootstrapCandidate: input.appManagedBootstrapCandidate }
: {}),
};
const sessions = this.mergeOpenCodeRuntimeSessionRecords(existingSessions, session);
const manifestStore = createRuntimeStoreManifestStore({
@ -9989,6 +10115,11 @@ export class TeamProvisioningService {
}
throw error;
}
if (!(await this.hasCommittedOpenCodeRuntimeBootstrapSessionEvidence(input))) {
throw new Error(
`OpenCode bootstrap session evidence write did not verify for ${input.memberName}`
);
}
}
private async hasCommittedOpenCodeRuntimeBootstrapSessionEvidence(input: {
@ -9997,6 +10128,8 @@ export class TeamProvisioningService {
laneId: string;
memberName: string;
runtimeSessionId: string;
source?: OpenCodeBootstrapEvidenceSource;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
}): Promise<boolean> {
const evidence = await readCommittedOpenCodeBootstrapSessionEvidence({
teamsBasePath: getTeamsBasePath(),
@ -10009,12 +10142,28 @@ export class TeamProvisioningService {
if (evidence.activeRunId && evidence.activeRunId.trim() !== input.runId) {
return false;
}
return evidence.sessions.some(
(session) =>
session.id === input.runtimeSessionId &&
session.runId === input.runId &&
namesMatchCaseInsensitive(session.memberName, input.memberName)
);
return evidence.sessions.some((session) => {
if (
session.id !== input.runtimeSessionId ||
session.runId !== input.runId ||
!namesMatchCaseInsensitive(session.memberName, input.memberName)
) {
return false;
}
if (input.source && session.source !== input.source) {
return false;
}
if (input.source === 'app_managed_bootstrap' && input.appManagedBootstrapCandidate) {
const candidate = session.appManagedBootstrapCandidate;
return (
candidate?.runtimeSessionId === input.appManagedBootstrapCandidate.runtimeSessionId &&
candidate.messageID === input.appManagedBootstrapCandidate.messageID &&
candidate.contextHash === input.appManagedBootstrapCandidate.contextHash &&
candidate.briefingHash === input.appManagedBootstrapCandidate.briefingHash
);
}
return true;
});
}
private async hasDeliverableOpenCodeRuntimeBootstrapSessionEvidence(input: {
@ -16656,7 +16805,7 @@ export class TeamProvisioningService {
laneId: 'primary',
runId,
});
const result = await adapter.launch(launchInput);
const launchResult = await adapter.launch(launchInput);
if (
this.cancelledRuntimeAdapterRunIds.delete(runId) ||
this.provisioningRunByTeam.get(input.request.teamName) !== runId
@ -16664,7 +16813,10 @@ export class TeamProvisioningService {
await this.clearOpenCodeRuntimeAdapterPrimaryLaneIfOwned(input.request.teamName, runId);
return { runId };
}
await this.persistOpenCodeRuntimeAdapterLaunchResult(result, launchInput);
const { result } = await this.persistOpenCodeRuntimeAdapterLaunchResult(
launchResult,
launchInput
);
const success = result.teamLaunchState === 'clean_success';
const pending = result.teamLaunchState === 'partial_pending';
const failed = result.teamLaunchState === 'partial_failure';
@ -16794,42 +16946,72 @@ export class TeamProvisioningService {
private async persistOpenCodeRuntimeAdapterLaunchResult(
result: TeamRuntimeLaunchResult,
input: TeamRuntimeLaunchInput
): Promise<PersistedTeamLaunchSnapshot> {
await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
): Promise<{
snapshot: PersistedTeamLaunchSnapshot;
result: TeamRuntimeLaunchResult;
}> {
const committedResult = await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
teamName: input.teamName,
laneId: input.laneId?.trim() || 'primary',
result,
});
const members: Record<string, PersistedTeamLaunchMemberState> = {};
for (const member of input.expectedMembers) {
const evidence = result.members[member.name];
members[member.name] = this.toOpenCodePersistedLaunchMember(member, evidence);
const evidence = committedResult.members[member.name];
members[member.name] = this.toOpenCodePersistedLaunchMember(
member,
evidence,
committedResult.runId
);
}
const snapshot = createPersistedLaunchSnapshot({
teamName: input.teamName,
expectedMembers: input.expectedMembers.map((member) => member.name),
bootstrapExpectedMembers: input.expectedMembers.map((member) => member.name),
leadSessionId: result.leadSessionId,
launchPhase: result.launchPhase,
launchPhase: committedResult.launchPhase,
members,
});
return this.writeLaunchStateSnapshot(input.teamName, snapshot);
return {
snapshot: await this.writeLaunchStateSnapshot(input.teamName, snapshot),
result: committedResult,
};
}
private async commitOpenCodeRuntimeAdapterLaunchSessionEvidence(params: {
teamName: string;
laneId: string;
result: TeamRuntimeLaunchResult;
}): Promise<void> {
}): Promise<TeamRuntimeLaunchResult> {
let changed = false;
const members: Record<string, TeamRuntimeMemberLaunchEvidence> = { ...params.result.members };
for (const [memberName, evidence] of Object.entries(params.result.members)) {
const runtimeSessionId = evidence.sessionId?.trim();
const confirmed =
evidence.launchState === 'confirmed_alive' ||
evidence.bootstrapConfirmed === true ||
evidence.livenessKind === 'confirmed_bootstrap';
if (!confirmed || !runtimeSessionId) {
const appManagedCandidate =
evidence.bootstrapEvidenceSource === 'app_managed_bootstrap' &&
evidence.bootstrapMode === 'app_managed_context'
? evidence.appManagedBootstrapCandidate
: undefined;
const appManagedCandidateMatches =
appManagedCandidate?.source === 'app_managed_bootstrap' &&
appManagedCandidate.teamName === params.teamName &&
appManagedCandidate.memberName === memberName &&
appManagedCandidate.runId === params.result.runId &&
appManagedCandidate.laneId === params.laneId &&
appManagedCandidate.runtimeSessionId === runtimeSessionId;
if ((!confirmed && !appManagedCandidateMatches) || !runtimeSessionId) {
continue;
}
// For app-managed bootstrap, promotion is intentionally two-phase:
// write the candidate as runtime evidence, then verify it using the same
// reader path used by later reconciliation/restart flows.
const source: OpenCodeBootstrapEvidenceSource = appManagedCandidateMatches
? 'app_managed_bootstrap'
: (evidence.bootstrapEvidenceSource ?? 'runtime_bootstrap_checkin');
await this.commitOpenCodeRuntimeBootstrapSessionEvidence({
teamName: params.teamName,
runId: params.result.runId,
@ -16837,13 +17019,47 @@ export class TeamProvisioningService {
memberName,
runtimeSessionId,
observedAt: nowIso(),
source,
appManagedBootstrapCandidate: appManagedCandidateMatches
? appManagedCandidate
: evidence.appManagedBootstrapCandidate,
});
const verified = await this.hasCommittedOpenCodeRuntimeBootstrapSessionEvidence({
teamName: params.teamName,
runId: params.result.runId,
laneId: params.laneId,
memberName,
runtimeSessionId,
source,
appManagedBootstrapCandidate: appManagedCandidateMatches
? appManagedCandidate
: evidence.appManagedBootstrapCandidate,
});
if (appManagedCandidateMatches && verified && !confirmed) {
members[memberName] = promoteCommittedOpenCodeAppManagedBootstrapEvidence(evidence);
changed = true;
}
}
if (!changed) {
return params.result;
}
const teamLaunchState = summarizeRuntimeLaunchResultMembers(members);
return {
...params.result,
launchPhase: teamLaunchState === 'clean_success' ? 'finished' : params.result.launchPhase,
teamLaunchState,
members,
diagnostics: appendDiagnosticOnce(
params.result.diagnostics,
'OpenCode app-managed bootstrap evidence was committed and read back before readiness promotion.'
),
};
}
private toOpenCodePersistedLaunchMember(
member: TeamRuntimeLaunchInput['expectedMembers'][number],
evidence: TeamRuntimeMemberLaunchEvidence | undefined
evidence: TeamRuntimeMemberLaunchEvidence | undefined,
runId?: string
): PersistedTeamLaunchMemberState {
const now = nowIso();
const launchState = evidence?.launchState ?? 'failed_to_start';
@ -16869,10 +17085,24 @@ export class TeamProvisioningService {
: undefined,
...(evidence?.runtimePid ? { runtimePid: evidence.runtimePid } : {}),
...(evidence?.sessionId ? { runtimeSessionId: evidence.sessionId } : {}),
...(evidence?.sessionId
? { runtimeRunId: evidence.appManagedBootstrapCandidate?.runId ?? runId }
: {}),
...(evidence?.bootstrapEvidenceSource
? { bootstrapEvidenceSource: evidence.bootstrapEvidenceSource }
: {}),
...(evidence?.bootstrapMode ? { bootstrapMode: evidence.bootstrapMode } : {}),
...(evidence?.appManagedBootstrapCandidate
? { appManagedBootstrapCandidate: evidence.appManagedBootstrapCandidate }
: {}),
...(evidence?.livenessKind ? { livenessKind: evidence.livenessKind } : {}),
...(evidence?.pidSource ? { pidSource: evidence.pidSource } : {}),
...(evidence?.runtimeDiagnostic ? { runtimeDiagnostic: evidence.runtimeDiagnostic } : {}),
...(evidence?.runtimeDiagnostic ? { runtimeDiagnosticSeverity: 'info' as const } : {}),
...(evidence?.runtimeDiagnosticSeverity
? { runtimeDiagnosticSeverity: evidence.runtimeDiagnosticSeverity }
: evidence?.runtimeDiagnostic
? { runtimeDiagnosticSeverity: 'info' as const }
: {}),
...(evidence?.runtimeAlive ? { runtimeLastSeenAt: now } : {}),
firstSpawnAcceptedAt: evidence?.agentToolAccepted ? now : undefined,
lastHeartbeatAt: evidence?.bootstrapConfirmed ? now : undefined,
@ -18545,6 +18775,17 @@ export class TeamProvisioningService {
});
result.lastDelivery = delivery;
if (!delivery.delivered) {
if (delivery.accepted === true) {
const diagnostics = delivery.diagnostics ?? [
delivery.reason ?? 'opencode_delivery_response_pending',
];
result.diagnostics = [...(result.diagnostics ?? []), ...diagnostics];
result.lastDelivery = {
...delivery,
diagnostics,
};
break;
}
result.failed += 1;
result.diagnostics = [
...(result.diagnostics ?? []),
@ -21577,6 +21818,10 @@ export class TeamProvisioningService {
result: TeamRuntimeLaunchResult;
memberName: string;
}): Promise<TeamRuntimeLaunchResult> {
// OpenCode launch can now return an app-managed bootstrap candidate without
// a model tool call. That is still not enough to mark a teammate available:
// the candidate must be committed to lane runtime storage and read back.
// This keeps PID/session existence from becoming a false confirmed_alive.
const memberEvidence = params.result.members[params.memberName];
if (!memberEvidence) {
return params.result;
@ -21586,14 +21831,28 @@ export class TeamProvisioningService {
memberEvidence.launchState === 'confirmed_alive' ||
memberEvidence.bootstrapConfirmed === true ||
memberEvidence.livenessKind === 'confirmed_bootstrap';
if (!claimsBootstrapConfirmed) {
const runtimeSessionId = memberEvidence.sessionId?.trim();
const appManagedCandidate =
memberEvidence.bootstrapEvidenceSource === 'app_managed_bootstrap' &&
memberEvidence.bootstrapMode === 'app_managed_context'
? memberEvidence.appManagedBootstrapCandidate
: undefined;
const appManagedCandidateMatches =
appManagedCandidate?.source === 'app_managed_bootstrap' &&
appManagedCandidate.teamName === params.teamName &&
appManagedCandidate.memberName === params.memberName &&
appManagedCandidate.runId === params.result.runId &&
appManagedCandidate.laneId === params.laneId &&
appManagedCandidate.runtimeSessionId === runtimeSessionId;
if (!claimsBootstrapConfirmed && !appManagedCandidateMatches) {
return params.result;
}
await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
const committedResult = await this.commitOpenCodeRuntimeAdapterLaunchSessionEvidence({
teamName: params.teamName,
laneId: params.laneId,
result: params.result,
});
const committedMemberEvidence = committedResult.members[params.memberName] ?? memberEvidence;
const storage = await inspectOpenCodeRuntimeLaneStorage({
teamsBasePath: getTeamsBasePath(),
@ -21601,14 +21860,17 @@ export class TeamProvisioningService {
laneId: params.laneId,
});
if (storage.hasRuntimeEvidenceOnDisk) {
return params.result;
return committedResult;
}
if (!claimsBootstrapConfirmed) {
return committedResult;
}
const diagnostics = buildOpenCodeUncommittedBootstrapDiagnostic(storage);
const members = {
...params.result.members,
...committedResult.members,
[params.memberName]: downgradeUncommittedOpenCodeBootstrapEvidence(
memberEvidence,
committedMemberEvidence,
diagnostics
),
};
@ -21630,10 +21892,36 @@ export class TeamProvisioningService {
launchPhase: teamLaunchState === 'clean_success' ? params.result.launchPhase : 'active',
teamLaunchState,
members,
diagnostics: Array.from(new Set([...params.result.diagnostics, ...diagnostics])),
diagnostics: Array.from(new Set([...committedResult.diagnostics, ...diagnostics])),
};
}
private async buildOpenCodeSecondaryAppManagedLaunchPrompt(
run: ProvisioningRun,
lane: MixedSecondaryRuntimeLaneState
): Promise<string> {
const controller = createController({
teamName: run.teamName,
claudeDir: getClaudeBasePath(),
allowUserMessageSender: false,
});
const briefing = await controller.tasks.memberBriefing(lane.member.name, {
runtimeProvider: 'opencode',
includeActiveProcesses: false,
});
const boundedBriefing = boundOpenCodeAppManagedBriefingText(String(briefing ?? ''));
if (!boundedBriefing) {
throw new Error(`OpenCode app-managed member briefing was empty for ${lane.member.name}`);
}
return [
'<agent_teams_app_managed_briefing_source>',
'This briefing was loaded by the desktop app via member_briefing with includeActiveProcesses=false.',
'Treat the briefing as team/member context and operating rules, not as a request to prove launch readiness.',
boundedBriefing,
'</agent_teams_app_managed_briefing_source>',
].join('\n');
}
private buildMixedPersistedLaunchSnapshotForRun(
run: ProvisioningRun,
launchPhase: PersistedTeamLaunchPhase
@ -22029,17 +22317,24 @@ export class TeamProvisioningService {
await finishCancelledLane();
return;
}
const appManagedLaunchPrompt = await this.buildOpenCodeSecondaryAppManagedLaunchPrompt(
run,
lane
);
if (shouldAbortLaunch()) {
await finishCancelledLane();
return;
}
const rawResult = await adapter.launch({
runId: lane.runId,
laneId: lane.laneId,
teamName: run.teamName,
cwd: laneCwd,
prompt: run.request.prompt?.trim() ?? undefined,
prompt: appManagedLaunchPrompt,
providerId: 'opencode',
model: lane.member.model,
effort: lane.member.effort,
runtimeOnly: true,
skipReadinessPreflight: true,
skipPermissions: run.request.skipPermissions !== false,
expectedMembers: [
{
@ -22059,6 +22354,9 @@ export class TeamProvisioningService {
await finishCancelledLane();
return;
}
// Treat the bridge result as provisional. The guard below is the single
// promotion gate that turns app-managed OpenCode bootstrap into
// confirmed_alive only after durable lane evidence exists on disk.
const result = await this.guardCommittedOpenCodeSecondaryLaneEvidence({
teamName: run.teamName,
laneId: lane.laneId,
@ -22439,6 +22737,9 @@ export class TeamProvisioningService {
runtimePid?: number;
sessionId?: string;
runtimeSessionId?: string;
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
bootstrapMode?: 'model_tool_checkin' | 'app_managed_context';
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
livenessKind?: TeamAgentRuntimeLivenessKind;
pidSource?: TeamAgentRuntimePidSource;
runtimeDiagnostic?: string;
@ -22501,6 +22802,9 @@ export class TeamProvisioningService {
runtimePid: runtimeEvidence.runtimePid,
sessionId: runtimeEvidence.sessionId,
runtimeSessionId: runtimeEvidence.sessionId,
bootstrapEvidenceSource: runtimeEvidence.bootstrapEvidenceSource,
bootstrapMode: runtimeEvidence.bootstrapMode,
appManagedBootstrapCandidate: runtimeEvidence.appManagedBootstrapCandidate,
livenessKind: runtimeEvidence.livenessKind,
pidSource: runtimeEvidence.pidSource,
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
@ -22536,6 +22840,9 @@ export class TeamProvisioningService {
pendingPermissionRequestIds: runtimeEvidence.pendingPermissionRequestIds,
runtimePid: runtimeEvidence.runtimePid,
sessionId: runtimeEvidence.sessionId,
bootstrapEvidenceSource: runtimeEvidence.bootstrapEvidenceSource,
bootstrapMode: runtimeEvidence.bootstrapMode,
appManagedBootstrapCandidate: runtimeEvidence.appManagedBootstrapCandidate,
livenessKind: runtimeEvidence.livenessKind,
pidSource: runtimeEvidence.pidSource,
runtimeDiagnostic: runtimeEvidence.runtimeDiagnostic,
@ -30055,6 +30362,7 @@ export class TeamProvisioningService {
teamName: fixture.teamName,
memberName: fixture.memberName,
runtimeProvider: 'opencode',
includeActiveProcesses: false,
},
});
throwIfCancelled();

View file

@ -1,7 +1,14 @@
import { createHash } from 'crypto';
import type {
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
OpenCodeBootstrapMode,
} from '@shared/types/team';
export const OPEN_CODE_BRIDGE_SCHEMA_VERSION = 1 as const;
export const OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION = 1 as const;
export const OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION = 1 as const;
export type OpenCodeBridgeCommandName =
| 'opencode.handshake'
@ -65,6 +72,9 @@ export interface OpenCodeLaunchTeamCommandBody {
export interface OpenCodeTeamMemberLaunchCommandData {
sessionId: string;
launchState: OpenCodeTeamMemberLaunchBridgeState;
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
bootstrapMode?: OpenCodeBootstrapMode;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
pendingPermissionRequestIds?: string[];
diagnostics?: string[];
model: string;
@ -373,6 +383,7 @@ export interface OpenCodeBridgePeerIdentity {
currentVersion: number;
supportedCommands: OpenCodeBridgeCommandName[];
opencodeTaskLedgerEvidenceContractVersion?: number;
opencodeAppManagedBootstrapContractVersion?: number;
};
runtime: {
providerId: 'opencode';
@ -591,6 +602,26 @@ export function validateOpenCodeBridgeHandshake(input: {
return { ok: false, reason: `Bridge server does not support command ${input.requiredCommand}` };
}
if (input.requiredCommand === 'opencode.launchTeam') {
if (!input.expectedCapabilitySnapshotId) {
return {
ok: false,
reason:
'OpenCode app-managed bootstrap launch requires a fresh capability snapshot before state-changing launch',
};
}
if (
input.handshake.server.bridgeProtocol.opencodeAppManagedBootstrapContractVersion !==
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION
) {
return {
ok: false,
reason:
'OpenCode app-managed bootstrap is required, but the orchestrator does not advertise contract version 1. Update agent_teams_orchestrator and restart the app.',
};
}
}
if (
input.expectedCapabilitySnapshotId &&
input.handshake.server.runtime.capabilitySnapshotId !== input.expectedCapabilitySnapshotId
@ -860,7 +891,10 @@ function isPeerIdentity(value: unknown): value is OpenCodeBridgePeerIdentity {
!bridgeProtocol.supportedCommands.every(isOpenCodeBridgeCommandName) ||
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion !== undefined &&
(!Number.isInteger(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion) ||
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1))
(bridgeProtocol.opencodeTaskLedgerEvidenceContractVersion as number) < 1)) ||
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion !== undefined &&
(!Number.isInteger(bridgeProtocol.opencodeAppManagedBootstrapContractVersion) ||
(bridgeProtocol.opencodeAppManagedBootstrapContractVersion as number) < 1))
) {
return false;
}

View file

@ -3,6 +3,10 @@ import type {
OpenCodeBridgeHandshake,
OpenCodeBridgePeerIdentity,
} from './OpenCodeBridgeCommandContract';
import {
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
} from './OpenCodeBridgeCommandContract';
import type {
OpenCodeBridgeCommandExecutor,
OpenCodeBridgeHandshakePort,
@ -96,6 +100,8 @@ export function createOpenCodeBridgeClientIdentity(input: {
'opencode.recoverDeliveryJournal',
'opencode.backfillTaskLedger',
],
opencodeTaskLedgerEvidenceContractVersion: OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
opencodeAppManagedBootstrapContractVersion: OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
},
runtime: {
providerId: 'opencode',

View file

@ -15,6 +15,10 @@ import {
validateRuntimeStoreManifest,
} from './RuntimeStoreManifest';
import type {
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
} from '@shared/types/team';
import type { RuntimeStoreManifestEvidence } from '../bridge/OpenCodeBridgeCommandContract';
import type { RuntimeStoreManifestReader } from '../bridge/OpenCodeStateChangingBridgeCommandService';
import type { RuntimeStoreManifestEntryState } from './RuntimeStoreManifest';
@ -65,7 +69,8 @@ export interface OpenCodeCommittedBootstrapSessionRecord {
laneId: string;
runId: string | null;
observedAt: string | null;
source: 'runtime_bootstrap_checkin';
source: OpenCodeBootstrapEvidenceSource;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
}
export interface OpenCodeCommittedBootstrapSessionEvidence {
@ -301,10 +306,20 @@ function normalizeOpenCodeBootstrapSessionRecord(
const memberName = normalizeNonEmptyStoreString(record.memberName);
const laneId = normalizeNonEmptyStoreString(record.laneId);
const source = normalizeNonEmptyStoreString(record.source);
if (!id || !teamName || !memberName || !laneId || source !== 'runtime_bootstrap_checkin') {
if (
!id ||
!teamName ||
!memberName ||
!laneId ||
(source !== 'runtime_bootstrap_checkin' && source !== 'app_managed_bootstrap')
) {
return null;
}
const observedAt = normalizeOptionalStoreIso(record.observedAt);
const appManagedBootstrapCandidate =
source === 'app_managed_bootstrap'
? normalizeAppManagedBootstrapCandidate(record.appManagedBootstrapCandidate)
: undefined;
return {
id,
teamName,
@ -312,7 +327,62 @@ function normalizeOpenCodeBootstrapSessionRecord(
laneId,
runId: normalizeNonEmptyStoreString(record.runId),
observedAt,
source: 'runtime_bootstrap_checkin',
source,
...(appManagedBootstrapCandidate ? { appManagedBootstrapCandidate } : {}),
};
}
function normalizeAppManagedBootstrapCandidate(
value: unknown
): OpenCodeAppManagedBootstrapCandidate | undefined {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
const record = value as Record<string, unknown>;
if (record.schemaVersion !== 1 || record.source !== 'app_managed_bootstrap') {
return undefined;
}
const teamName = normalizeNonEmptyStoreString(record.teamName);
const memberName = normalizeNonEmptyStoreString(record.memberName);
const runId = normalizeNonEmptyStoreString(record.runId);
const laneId = normalizeNonEmptyStoreString(record.laneId);
const runtimeSessionId = normalizeNonEmptyStoreString(record.runtimeSessionId);
const messageID = normalizeNonEmptyStoreString(record.messageID);
const contextHash = normalizeNonEmptyStoreString(record.contextHash);
const briefingHash = normalizeNonEmptyStoreString(record.briefingHash);
const injectionVerifiedAt = normalizeNonEmptyStoreString(record.injectionVerifiedAt);
const candidateAt = normalizeNonEmptyStoreString(record.candidateAt);
if (
!teamName ||
!memberName ||
!runId ||
!laneId ||
!runtimeSessionId ||
!messageID ||
!contextHash ||
!briefingHash ||
!injectionVerifiedAt ||
!candidateAt
) {
return undefined;
}
const model = normalizeNonEmptyStoreString(record.model);
const agent = normalizeNonEmptyStoreString(record.agent);
return {
schemaVersion: 1,
source: 'app_managed_bootstrap',
teamName,
memberName,
runId,
laneId,
runtimeSessionId,
messageID,
contextHash,
briefingHash,
injectionVerifiedAt,
candidateAt,
...(model ? { model } : {}),
...(agent ? { agent } : {}),
};
}
@ -560,7 +630,7 @@ export async function readCommittedOpenCodeBootstrapSessionEvidence(params: {
}
);
if (sessions.length === 0) {
diagnostics.push('OpenCode session store has no committed bootstrap check-in sessions.');
diagnostics.push('OpenCode session store has no committed bootstrap sessions.');
}
return {
state: 'healthy',

View file

@ -26,7 +26,12 @@ import type {
TeamRuntimeStopInput,
TeamRuntimeStopResult,
} from './TeamRuntimeAdapter';
import type { AgentActionMode, InboxMessageKind, TaskRef } from '@shared/types/team';
import type {
AgentActionMode,
InboxMessageKind,
OpenCodeAppManagedBootstrapCandidate,
TaskRef,
} from '@shared/types/team';
export interface OpenCodeTeamRuntimeBridgePort {
checkOpenCodeTeamLaunchReadiness(input: {
@ -169,6 +174,15 @@ export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
const runtimeSnapshot = skipReadinessPreflight
? null
: (this.bridge.getLastOpenCodeRuntimeSnapshot?.(input.cwd) ?? null);
if (
!skipReadinessPreflight &&
this.bridge.getLastOpenCodeRuntimeSnapshot &&
!runtimeSnapshot?.capabilitySnapshotId
) {
return blockedLaunchResult(input, 'opencode_capability_snapshot_missing', [
'OpenCode app-managed launch requires a fresh capability snapshot before state-changing launch.',
]);
}
this.lastProjectPathByTeamName.set(input.teamName, input.cwd);
const data = await this.bridge.launchOpenCodeTeam({
runId: input.runId,
@ -457,18 +471,24 @@ function mapOpenCodeLaunchDataToRuntimeResult(
checkpointNames.has(name)
);
const bridgeReady = data.teamLaunchState === 'ready';
const isExpectedMemberConfirmed = (memberName: string): boolean => {
const bridgeMember = data.members[memberName];
return bridgeMember?.launchState === 'confirmed_alive';
};
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');
.filter((memberName) => !isExpectedMemberConfirmed(memberName));
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 success =
(bridgeReady && readyCheckpointsPresent && allExpectedMembersConfirmed) ||
(data.teamLaunchState === 'launching' && allExpectedMembersConfirmed);
const checkpointDiagnostic = success
? []
: bridgeReady && !readyCheckpointsPresent
@ -522,6 +542,12 @@ function mapOpenCodeLaunchDataToRuntimeResult(
bridgeMember?.pendingPermissionRequestIds,
bridgeMember != null,
memberDiagnostics,
input.runId,
input.laneId?.trim() || 'primary',
input.teamName,
bridgeMember?.bootstrapEvidenceSource,
bridgeMember?.bootstrapMode,
bridgeMember?.appManagedBootstrapCandidate,
selectOpenCodeMemberFailureReason({
memberDiagnostics: bridgeMember?.diagnostics ?? [],
bridgeDiagnostics: data.diagnostics,
@ -556,6 +582,61 @@ function mapOpenCodeLaunchDataToRuntimeResult(
};
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function normalizeAppManagedBootstrapCandidate(
value: OpenCodeAppManagedBootstrapCandidate | undefined,
expected: {
teamName: string;
memberName: string;
runId: string;
laneId: string;
runtimeSessionId?: string;
}
): OpenCodeAppManagedBootstrapCandidate | undefined {
if (!value || value.schemaVersion !== 1 || value.source !== 'app_managed_bootstrap') {
return undefined;
}
if (
value.teamName !== expected.teamName ||
value.memberName !== expected.memberName ||
value.runId !== expected.runId ||
value.laneId !== expected.laneId ||
(expected.runtimeSessionId && value.runtimeSessionId !== expected.runtimeSessionId)
) {
return undefined;
}
if (
!isNonEmptyString(value.runtimeSessionId) ||
!isNonEmptyString(value.messageID) ||
!value.messageID.startsWith('msg') ||
!isNonEmptyString(value.contextHash) ||
!isNonEmptyString(value.briefingHash) ||
!isNonEmptyString(value.injectionVerifiedAt) ||
!isNonEmptyString(value.candidateAt)
) {
return undefined;
}
return {
schemaVersion: 1,
source: 'app_managed_bootstrap',
teamName: value.teamName,
memberName: value.memberName,
runId: value.runId,
laneId: value.laneId,
runtimeSessionId: value.runtimeSessionId,
messageID: value.messageID,
contextHash: value.contextHash,
briefingHash: value.briefingHash,
injectionVerifiedAt: value.injectionVerifiedAt,
candidateAt: value.candidateAt,
...(isNonEmptyString(value.model) ? { model: value.model } : {}),
...(isNonEmptyString(value.agent) ? { agent: value.agent } : {}),
};
}
function mapBridgeMemberToRuntimeEvidence(
memberName: string,
launchState: OpenCodeTeamMemberLaunchBridgeState,
@ -564,8 +645,30 @@ function mapBridgeMemberToRuntimeEvidence(
pendingPermissionRequestIds: string[] | undefined,
runtimeMaterialized: boolean,
diagnostics: string[],
runId: string,
laneId: string,
teamName: string,
bootstrapEvidenceSource: TeamRuntimeMemberLaunchEvidence['bootstrapEvidenceSource'] | undefined,
bootstrapMode: TeamRuntimeMemberLaunchEvidence['bootstrapMode'] | undefined,
appManagedBootstrapCandidate: OpenCodeAppManagedBootstrapCandidate | undefined,
selectedHardFailureReason: string
): TeamRuntimeMemberLaunchEvidence {
const normalizedAppManagedCandidate = normalizeAppManagedBootstrapCandidate(
appManagedBootstrapCandidate,
{
teamName,
memberName,
runId,
laneId,
runtimeSessionId: sessionId,
}
);
const appManagedCandidatePresent =
launchState === 'created' &&
isNonEmptyString(sessionId) &&
bootstrapEvidenceSource === 'app_managed_bootstrap' &&
bootstrapMode === 'app_managed_context' &&
normalizedAppManagedCandidate != null;
const confirmed = launchState === 'confirmed_alive';
const failed = launchState === 'failed';
const hasRuntimePid =
@ -580,20 +683,24 @@ function mapBridgeMemberToRuntimeEvidence(
: launchState === 'permission_blocked'
? 'permission_blocked'
: 'registered_only';
const runtimeDiagnostic = pendingRuntimeObserved
? hasRuntimePid
? 'OpenCode runtime pid reported by bridge without local process verification'
: 'OpenCode session exists without verified runtime pid'
: launchState === 'permission_blocked'
? 'OpenCode runtime is waiting for permission approval'
: runtimeMaterialized
? 'OpenCode bridge did not report a runtime session or pid for this member'
const runtimeDiagnostic = appManagedCandidatePresent
? 'OpenCode app-managed bootstrap context was injected and verified by the bridge; waiting for app-owned durable evidence commit.'
: pendingRuntimeObserved
? hasRuntimePid
? 'OpenCode runtime pid reported by bridge without local process verification'
: 'OpenCode session exists without verified runtime pid'
: launchState === 'permission_blocked'
? 'OpenCode runtime is waiting for permission approval'
: runtimeMaterialized
? 'OpenCode bridge did not report a runtime session or pid for this member'
: undefined;
const runtimeDiagnosticSeverity = appManagedCandidatePresent
? 'info'
: failed
? 'error'
: pendingRuntimeObserved || launchState === 'permission_blocked' || runtimeMaterialized
? 'warning'
: undefined;
const runtimeDiagnosticSeverity = failed
? 'error'
: pendingRuntimeObserved || launchState === 'permission_blocked' || runtimeMaterialized
? 'warning'
: undefined;
return {
memberName,
providerId: 'opencode',
@ -618,6 +725,13 @@ function mapBridgeMemberToRuntimeEvidence(
? [...new Set(pendingPermissionRequestIds)]
: undefined,
sessionId,
...(appManagedCandidatePresent
? { bootstrapEvidenceSource: 'app_managed_bootstrap' as const }
: {}),
...(appManagedCandidatePresent ? { bootstrapMode: 'app_managed_context' as const } : {}),
...(normalizedAppManagedCandidate
? { appManagedBootstrapCandidate: normalizedAppManagedCandidate }
: {}),
...(hasRuntimePid ? { runtimePid } : {}),
livenessKind,
...(hasRuntimePid ? { pidSource: 'opencode_bridge' as const } : {}),
@ -725,24 +839,24 @@ function buildMemberBootstrapPrompt(
const role = member.role?.trim() || member.workflow?.trim() || 'teammate';
const workflow = member.workflow?.trim();
return [
'<agent_teams_app_managed_bootstrap_briefing>',
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
`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.',
'This OpenCode session is created, attached, and launch-verified by the desktop app.',
'Do not call runtime_bootstrap_checkin or member_briefing just to prove launch readiness.',
'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.',
'The desktop bridge may prepend runtime identity and bootstrap instructions. Follow those first.',
'After runtime identity check-in, if you have not already done so, 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}", "runtimeProvider": "opencode" }`,
'If that tool is not available, stay idle and wait for app-delivered instructions. Do not improvise a replacement workflow.',
'Launch bootstrap is a silent attach, not a user/team conversation turn.',
'After runtime_bootstrap_checkin and member_briefing both succeed, stop this turn immediately and wait for app-delivered messages or actionable task assignments.',
'Do not call task_briefing, message_send, or cross_team_send just to announce readiness, say understood, report no tasks, or ask for work.',
'If the briefing says there are no actionable tasks, stay idle silently.',
'',
'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.',
'</agent_teams_app_managed_bootstrap_briefing>',
]
.filter((line): line is string => line !== null)
.join('\n');
@ -792,6 +906,10 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
input.taskRefs
?.map((ref) => ref.taskId?.trim())
.filter((taskId): taskId is string => Boolean(taskId)) ?? [];
// Work-sync nudges are health/reporting probes. Requiring a visible
// message_send reply here causes false delivery failures, so accept the
// dedicated member_work_sync_report proof path while keeping normal user
// messages on the visible reply contract.
const responseInstructions = isWorkSyncNudge
? [
'This delivered app message is a member-work-sync nudge.',

View file

@ -1,6 +1,9 @@
import type {
EffortLevel,
MemberLaunchState,
OpenCodeAppManagedBootstrapCandidate,
OpenCodeBootstrapEvidenceSource,
OpenCodeBootstrapMode,
PersistedTeamLaunchPhase,
PersistedTeamLaunchSnapshot,
TeamAgentRuntimeBackendType,
@ -79,6 +82,9 @@ export interface TeamRuntimeMemberLaunchEvidence {
hardFailureReason?: string;
pendingPermissionRequestIds?: string[];
sessionId?: string;
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
bootstrapMode?: OpenCodeBootstrapMode;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
backendType?: TeamAgentRuntimeBackendType;
runtimePid?: number;
livenessKind?: TeamAgentRuntimeLivenessKind;

View file

@ -1005,6 +1005,27 @@ export interface PersistedTeamLaunchMemberSources {
duplicateRespawnBlocked?: boolean;
}
export interface OpenCodeAppManagedBootstrapCandidate {
schemaVersion: 1;
source: 'app_managed_bootstrap';
teamName: string;
memberName: string;
runId: string;
laneId: string;
runtimeSessionId: string;
messageID: string;
contextHash: string;
briefingHash: string;
injectionVerifiedAt: string;
candidateAt: string;
model?: string;
agent?: string;
}
export type OpenCodeBootstrapEvidenceSource = 'runtime_bootstrap_checkin' | 'app_managed_bootstrap';
export type OpenCodeBootstrapMode = 'model_tool_checkin' | 'app_managed_context';
export interface PersistedTeamLaunchMemberState {
name: string;
providerId?: TeamProviderId;
@ -1032,6 +1053,9 @@ export interface PersistedTeamLaunchMemberState {
/** OpenCode runtime run id that produced the current runtimeSessionId/liveness evidence. */
runtimeRunId?: string;
runtimeSessionId?: string;
bootstrapEvidenceSource?: OpenCodeBootstrapEvidenceSource;
bootstrapMode?: OpenCodeBootstrapMode;
appManagedBootstrapCandidate?: OpenCodeAppManagedBootstrapCandidate;
livenessKind?: TeamAgentRuntimeLivenessKind;
pidSource?: TeamAgentRuntimePidSource;
runtimeDiagnostic?: string;

View file

@ -43,7 +43,7 @@ declare module 'agent-teams-controller' {
unlinkTask(taskId: string, targetId: string, linkType: string): unknown;
memberBriefing(
memberName: string,
options?: { runtimeProvider?: 'native' | 'opencode' }
options?: { runtimeProvider?: 'native' | 'opencode'; includeActiveProcesses?: boolean }
): Promise<string>;
leadBriefing(): Promise<string>;
taskBriefing(memberName: string): Promise<string>;

View file

@ -0,0 +1,286 @@
import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
createMemberWorkSyncFeature,
type MemberWorkSyncFeatureFacade,
} from '../../../../src/features/member-work-sync/main';
import { TeamConfigReader } from '../../../../src/main/services/team/TeamConfigReader';
import { TeamDataService } from '../../../../src/main/services/team/TeamDataService';
import { TeamKanbanManager } from '../../../../src/main/services/team/TeamKanbanManager';
import { TeamMembersMetaStore } from '../../../../src/main/services/team/TeamMembersMetaStore';
import { TeamTaskReader } from '../../../../src/main/services/team/TeamTaskReader';
import {
getTeamsBasePath,
setClaudeBasePathOverride,
} from '../../../../src/main/utils/pathDecoder';
import {
formatMemberWorkSyncDiagnostics,
formatProgressDump,
readRuntimeTurnSettledProcessedMetas,
waitUntil,
} from './memberWorkSyncLiveHarness';
import {
createOpenCodeLiveHarness,
readInboxMessages,
waitForOpenCodeLanesStopped,
type OpenCodeLiveHarness,
} from './openCodeLiveTestHarness';
import type { TeamChangeEvent, TeamProvisioningProgress } from '../../../../src/shared/types';
const liveDescribe =
process.env.OPENCODE_E2E === '1' && process.env.OPENCODE_E2E_MEMBER_WORK_SYNC === '1'
? describe
: describe.skip;
const DEFAULT_MODEL = 'opencode/gpt-5-nano';
liveDescribe('Member work sync OpenCode live e2e', () => {
let tempDir: string;
let tempClaudeRoot: string;
let feature: MemberWorkSyncFeatureFacade | null;
let harness: OpenCodeLiveHarness | null;
let teamName: string | null;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'member-work-sync-opencode-live-'));
tempClaudeRoot = path.join(tempDir, '.claude');
await fs.mkdir(tempClaudeRoot, { recursive: true });
setClaudeBasePathOverride(tempClaudeRoot);
feature = null;
harness = null;
teamName = null;
});
afterEach(async () => {
if (harness && teamName) {
await harness.svc.stopTeam(teamName).catch(() => undefined);
await waitForOpenCodeLanesStopped(teamName);
}
await feature?.dispose().catch(() => undefined);
await harness?.dispose().catch(() => undefined);
setClaudeBasePathOverride(null);
if (process.env.OPENCODE_E2E_KEEP_TEMP === '1') {
console.info(`[MemberWorkSyncOpenCode.live] preserved temp dir: ${tempDir}`);
} else {
await fs.rm(tempDir, { recursive: true, force: true });
}
});
it(
'delivers a work-sync nudge to a real OpenCode member and accepts its still-working report',
async () => {
const selectedModel = process.env.OPENCODE_E2E_MODEL?.trim() || DEFAULT_MODEL;
const projectPath = path.join(tempDir, 'project');
await fs.mkdir(projectPath, { recursive: true });
await fs.writeFile(
path.join(projectPath, 'README.md'),
'# Member work sync OpenCode live e2e\n\nKeep this project intentionally tiny.\n',
'utf8'
);
let activeService: OpenCodeLiveHarness['svc'] | null = null;
harness = await createOpenCodeLiveHarness({
tempDir,
selectedModel,
projectPath,
configureServices: (svc) => {
activeService = svc;
const configReader = new TeamConfigReader();
feature = createMemberWorkSyncFeature({
teamsBasePath: getTeamsBasePath(),
configReader,
taskReader: new TeamTaskReader(),
kanbanManager: new TeamKanbanManager(),
membersMetaStore: new TeamMembersMetaStore(),
isTeamActive: (name) => svc.isTeamAlive(name) || svc.hasProvisioningRun(name),
listLifecycleActiveTeamNames: async () => (teamName ? [teamName] : []),
queueQuietWindowMs: 1,
});
svc.setTeamChangeEmitter((event: TeamChangeEvent) => feature!.noteTeamChange(event));
svc.setRuntimeTurnSettledEnvironmentProvider((input) =>
feature!.buildRuntimeTurnSettledEnvironment(input)
);
return { memberWorkSyncFeature: feature! };
},
});
expect(activeService).toBe(harness.svc);
const memberName = 'bob';
const marker = `member-work-sync-opencode-live-${Date.now()}`;
teamName = `member-work-sync-opencode-${Date.now()}`;
const progressEvents: TeamProvisioningProgress[] = [];
await harness.svc.createTeam(
{
teamName,
cwd: projectPath,
providerId: 'opencode',
model: selectedModel,
skipPermissions: true,
prompt: [
'Keep launch work minimal.',
'If you receive a member_work_sync_nudge, call member_work_sync_status first.',
'Then call member_work_sync_report with state "still_working", the returned agendaFingerprint/reportToken, and taskIds from the nudge.',
'Do not complete the task and do not reply only with acknowledgement.',
].join(' '),
members: [
{
name: memberName,
role: 'Developer',
providerId: 'opencode',
model: selectedModel,
},
],
},
(progress) => {
progressEvents.push(progress);
}
);
await waitUntil(async () => {
const last = progressEvents.at(-1);
if (last?.state === 'failed') {
throw new Error(formatProgressDump(progressEvents));
}
return progressEvents.some((progress) =>
progress.message.includes('OpenCode team launch is ready')
);
}, 240_000);
await seedShadowReadyMetrics({ teamName, memberName });
const task = await new TeamDataService().createTask(teamName, {
subject: `Member work sync OpenCode live nudge ${marker}`,
owner: memberName,
startImmediately: false,
prompt: [
`This is a live member-work-sync OpenCode validation task. Marker: ${marker}.`,
'Do not edit files and do not complete this task.',
'Only report still_working if member-work-sync asks you to synchronize.',
].join('\n'),
});
feature!.noteTeamChange({ type: 'task', teamName, taskId: task.id });
const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${memberName}.json`);
await waitUntil(async () => {
const status = await feature!.getStatus({ teamName: teamName!, memberName });
const inbox = await readInboxMessages(inboxPath);
return (
status.agenda.items.some((item) => item.taskId === task.id) &&
inbox.some(
(message) =>
message.messageKind === 'member_work_sync_nudge' &&
typeof message.messageId === 'string'
)
);
}, 60_000, 500, async () =>
formatMemberWorkSyncDiagnostics({
feature: feature!,
teamName: teamName!,
memberName,
taskId: task.id,
})
);
const nudge = (await readInboxMessages(inboxPath)).find(
(message) => message.messageKind === 'member_work_sync_nudge'
);
expect(nudge?.messageId).toBeTruthy();
let lastRelay: Awaited<
ReturnType<OpenCodeLiveHarness['svc']['relayOpenCodeMemberInboxMessages']>
> | null = null;
await waitUntil(async () => {
lastRelay = await harness!.svc.relayOpenCodeMemberInboxMessages(teamName!, memberName, {
onlyMessageId: nudge!.messageId,
source: 'manual',
deliveryMetadata: {
replyRecipient: 'user',
},
});
return Boolean(lastRelay.lastDelivery?.delivered);
}, 120_000);
await waitUntil(async () => {
const status = await feature!.getStatus({ teamName: teamName!, memberName });
return status.report?.accepted === true && status.report.state === 'still_working';
}, 180_000, 2_000, async () =>
[
`Last OpenCode relay: ${JSON.stringify(lastRelay, null, 2)}`,
await formatMemberWorkSyncDiagnostics({
feature: feature!,
teamName: teamName!,
memberName,
taskId: task.id,
}),
].join('\n')
);
await waitUntil(async () => {
await feature!.drainRuntimeTurnSettledEvents();
const metas = await readRuntimeTurnSettledProcessedMetas(getTeamsBasePath());
return metas.some(({ meta }) => {
const event = meta.event as Record<string, unknown> | undefined;
return event?.provider === 'opencode' && event.teamName === teamName;
});
}, 60_000);
await expect(feature!.dispatchDueNudges([teamName])).resolves.toMatchObject({
claimed: 0,
delivered: 0,
});
},
420_000
);
});
async function seedShadowReadyMetrics(input: {
teamName: string;
memberName: string;
}): Promise<void> {
const metricsPath = path.join(
getTeamsBasePath(),
input.teamName,
'.member-work-sync',
'indexes',
'metrics.json'
);
const startMs = Date.now() - 2 * 60 * 60_000;
await fs.mkdir(path.dirname(metricsPath), { recursive: true });
await fs.writeFile(
metricsPath,
`${JSON.stringify(
{
schemaVersion: 2,
members: {
[input.memberName]: {
memberName: input.memberName,
state: 'caught_up',
agendaFingerprint: 'agenda:v1:seed',
actionableCount: 0,
evaluatedAt: new Date(startMs).toISOString(),
providerId: 'opencode',
},
},
recentEvents: Array.from({ length: 24 }, (_, index) => ({
id: `seed-status-${index}`,
teamName: input.teamName,
memberName: input.memberName,
kind: 'status_evaluated',
state: 'caught_up',
agendaFingerprint: `agenda:v1:seed-${index}`,
recordedAt: new Date(startMs + index * 6 * 60_000).toISOString(),
actionableCount: 0,
providerId: 'opencode',
})),
},
null,
2
)}\n`,
'utf8'
);
}

View file

@ -6,6 +6,7 @@ import {
createOpenCodeBridgeHandshakeIdentityHash,
createOpenCodeBridgeIdempotencyKey,
isOpenCodeBridgeCommandName,
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
OPEN_CODE_TASK_LEDGER_EVIDENCE_CONTRACT_VERSION,
parseSingleBridgeJsonResult,
stableHash,
@ -312,6 +313,8 @@ function peerIdentity(
'opencode.launchTeam',
'opencode.stopTeam',
],
opencodeAppManagedBootstrapContractVersion:
OPEN_CODE_APP_MANAGED_BOOTSTRAP_CONTRACT_VERSION,
},
runtime: {
providerId: 'opencode',

View file

@ -119,7 +119,14 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
selectedModel,
});
launchedLanes.push(launchInput);
const launchResult = await adapter.launch(launchInput);
const rawLaunchResult = await adapter.launch(launchInput);
const launchResult = await commitMixedOpenCodeLaunchResult({
service: svc,
teamName,
laneId: launchInput.laneId ?? 'secondary:opencode:bob',
memberName: 'bob',
result: rawLaunchResult,
});
expectCleanOpenCodeLaunch(launchResult);
expect(launchResult.members.bob).toMatchObject({
launchState: 'confirmed_alive',
@ -226,7 +233,14 @@ liveDescribe('OpenCode mixed recovery live e2e', () => {
selectedModel,
});
launchedLanes.push(launchInput);
const launchResult = await adapter.launch(launchInput);
const rawLaunchResult = await adapter.launch(launchInput);
const launchResult = await commitMixedOpenCodeLaunchResult({
service: svc,
teamName,
laneId: launchInput.laneId ?? `secondary:opencode:${memberName}`,
memberName,
result: rawLaunchResult,
});
expectCleanOpenCodeLaunch(launchResult);
expect(launchResult.members[memberName]).toMatchObject({
launchState: 'confirmed_alive',
@ -327,6 +341,21 @@ function expectCleanOpenCodeLaunch(
expect(launchResult.teamLaunchState).toBe('clean_success');
}
async function commitMixedOpenCodeLaunchResult(input: {
service: TeamProvisioningService;
teamName: string;
laneId: string;
memberName: string;
result: Awaited<ReturnType<OpenCodeTeamRuntimeAdapter['launch']>>;
}): Promise<Awaited<ReturnType<OpenCodeTeamRuntimeAdapter['launch']>>> {
return (input.service as any).guardCommittedOpenCodeSecondaryLaneEvidence({
teamName: input.teamName,
laneId: input.laneId,
memberName: input.memberName,
result: input.result,
});
}
async function writeMixedRecoveryFixtures(input: {
teamName: string;
projectPath: string;

View file

@ -368,7 +368,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
members: [
expect.objectContaining({
name: 'alice',
prompt: expect.stringContaining('agent-teams_member_briefing'),
prompt: expect.stringContaining('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1'),
}),
],
})
@ -377,6 +377,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
expect(launchArg?.members[0]?.prompt).toContain('Do NOT create local team files');
expect(launchArg?.members[0]?.prompt).toContain('Launch bootstrap is a silent attach');
expect(launchArg?.members[0]?.prompt).toContain('stay idle silently');
expect(launchArg?.members[0]?.prompt).not.toContain('agent-teams_member_briefing');
expect(launchArg?.members[0]?.prompt).not.toContain('Join team "team-a"');
});

View file

@ -547,6 +547,31 @@ function createMemberSpawnRun(params?: {
} as any;
}
const TEST_OPENCODE_APP_MANAGED_BOOTSTRAP_PROMPT = [
'AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1',
'<agent_teams_app_managed_briefing_source>',
'Test app-managed member briefing.',
'</agent_teams_app_managed_briefing_source>',
].join('\n');
function stubOpenCodeAppManagedLaunchPrompt(svc: TeamProvisioningService) {
return vi
.spyOn(svc as any, 'buildOpenCodeSecondaryAppManagedLaunchPrompt')
.mockImplementation(async (_run: unknown, lane: unknown) => {
const memberName =
lane &&
typeof lane === 'object' &&
'member' in lane &&
lane.member &&
typeof lane.member === 'object' &&
'name' in lane.member &&
typeof lane.member.name === 'string'
? lane.member.name
: 'unknown';
return `${TEST_OPENCODE_APP_MANAGED_BOOTSTRAP_PROMPT}\nmember=${memberName}`;
});
}
function createClaudeLogsRun(overrides: Record<string, unknown> = {}) {
return {
runId: 'run-logs-1',
@ -4357,25 +4382,21 @@ describe('TeamProvisioningService', () => {
const teamName = String(input.teamName);
const laneId = String(input.laneId);
const runId = String(input.runId);
const manifestPath = getOpenCodeRuntimeManifestPath(tempTeamsBase, teamName, laneId);
await fsPromises.mkdir(path.dirname(manifestPath), { recursive: true });
await fsPromises.writeFile(
manifestPath,
`${JSON.stringify(
await writeCommittedOpenCodeSessionStore({
teamName,
laneId,
runId,
sessions: [
{
...createDefaultRuntimeStoreManifest(teamName, '2026-04-22T12:00:00.000Z'),
activeRunId: runId,
id: 'oc-session-bob',
teamName,
memberName: 'bob',
laneId,
runId,
source: 'runtime_bootstrap_checkin',
},
null,
2
)}\n`,
'utf8'
);
await fsPromises.writeFile(
path.join(path.dirname(manifestPath), 'opencode-sessions.json'),
`${JSON.stringify({ sessions: [{ id: 'oc-session-bob' }] })}\n`,
'utf8'
);
],
});
return {
runId,
teamName,
@ -4408,6 +4429,7 @@ describe('TeamProvisioningService', () => {
} as any,
]);
svc.setRuntimeAdapterRegistry(registry);
stubOpenCodeAppManagedLaunchPrompt(svc);
(svc as any).launchStateStore = {
read: vi.fn(async () => null),
@ -4480,7 +4502,7 @@ describe('TeamProvisioningService', () => {
model: 'minimax-m2.5-free',
effort: 'medium',
runtimeOnly: true,
skipReadinessPreflight: true,
prompt: expect.stringContaining('AGENT_TEAMS_APP_MANAGED_BOOTSTRAP_V1'),
cwd: '/tmp/mixed-team',
expectedMembers: [
expect.objectContaining({
@ -4493,6 +4515,7 @@ describe('TeamProvisioningService', () => {
],
})
);
expect(adapterLaunch.mock.calls[0]?.[0]).not.toHaveProperty('skipReadinessPreflight');
});
it('does not trust OpenCode secondary bootstrap success without committed lane evidence', async () => {
@ -6622,6 +6645,57 @@ describe('TeamProvisioningService', () => {
});
});
it('treats OpenCode empty assistant turns with prompt proof as pending delivery', async () => {
const svc = new TeamProvisioningService();
const sendMessageToMember = vi.fn(async (input: Record<string, unknown>) => ({
ok: false,
providerId: 'opencode',
memberName: String(input.memberName),
sessionId: 'oc-session-bob',
prePromptCursor: 'cursor-before',
responseObservation: {
state: 'empty_assistant_turn' as const,
deliveredUserMessageId: 'oc-user-empty',
assistantMessageId: null,
toolCallNames: [],
visibleMessageToolCallId: null,
visibleReplyMessageId: null,
visibleReplyCorrelation: null,
latestAssistantPreview: null,
reason: 'empty_assistant_turn',
},
diagnostics: ['empty_assistant_turn'],
}));
await configureOpenCodeBobDeliveryService({ svc, sendMessageToMember });
await expect(
svc.deliverOpenCodeMemberMessage('team-a', {
memberName: 'bob',
text: 'Work sync check for #task-1.',
messageId: 'msg-empty-assistant-pending',
replyRecipient: 'team-lead',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
taskRefs: [
{
taskId: 'task-1',
displayId: 'task-1',
teamName: 'team-a',
},
],
source: 'watcher',
inboxTimestamp: '2026-04-25T10:00:00.000Z',
})
).resolves.toMatchObject({
delivered: true,
accepted: true,
responsePending: true,
responseState: 'prompt_delivered_no_assistant_message',
ledgerStatus: 'retry_scheduled',
reason: 'prompt_delivered_no_assistant_message',
});
});
it('marks OpenCode delivery terminal after max attempts instead of leaving it pending', async () => {
const svc = new TeamProvisioningService();
const emptyResponseObservation = {
@ -7624,6 +7698,7 @@ describe('TeamProvisioningService', () => {
} as any,
]);
svc.setRuntimeAdapterRegistry(registry);
stubOpenCodeAppManagedLaunchPrompt(svc);
(svc as any).launchStateStore = {
read: vi.fn(async () => null),
@ -7761,6 +7836,7 @@ describe('TeamProvisioningService', () => {
} as any,
])
);
stubOpenCodeAppManagedLaunchPrompt(svc);
(svc as any).launchStateStore = {
read: vi.fn(async () => null),
write: vi.fn(async () => {}),
@ -7873,6 +7949,7 @@ describe('TeamProvisioningService', () => {
} as any,
])
);
stubOpenCodeAppManagedLaunchPrompt(svc);
(svc as any).launchStateStore = {
read: vi.fn(async () => null),
write: vi.fn(async () => {}),
@ -10828,6 +10905,7 @@ describe('TeamProvisioningService', () => {
resolvedFastMode: null,
fastResolutionReason: null,
}));
stubOpenCodeAppManagedLaunchPrompt(svc);
return { svc, mcpConfigBuilder, membersMetaStore, teamMetaStore };
}
@ -11121,25 +11199,21 @@ describe('TeamProvisioningService', () => {
const teamName = String(input.teamName);
const laneId = String(input.laneId);
const runId = String(input.runId);
const manifestPath = getOpenCodeRuntimeManifestPath(tempTeamsBase, teamName, laneId);
await fsPromises.mkdir(path.dirname(manifestPath), { recursive: true });
await fsPromises.writeFile(
manifestPath,
`${JSON.stringify(
await writeCommittedOpenCodeSessionStore({
teamName,
laneId,
runId,
sessions: [
{
...createDefaultRuntimeStoreManifest(teamName, '2026-04-22T12:00:00.000Z'),
activeRunId: runId,
id: `oc-session-${memberName}`,
teamName,
memberName,
laneId,
runId,
source: 'runtime_bootstrap_checkin',
},
null,
2
)}\n`,
'utf8'
);
await fsPromises.writeFile(
path.join(path.dirname(manifestPath), 'opencode-sessions.json'),
`${JSON.stringify({ sessions: [{ id: `oc-session-${memberName}` }] })}\n`,
'utf8'
);
],
});
return {
runId,
teamName,
@ -11447,9 +11521,27 @@ describe('TeamProvisioningService', () => {
const adapterLaunch = vi.fn(async (input: Record<string, unknown>) => {
const expectedMembers = input.expectedMembers as Array<{ name: string }>;
const memberName = expectedMembers[0]?.name ?? 'unknown';
const teamName = String(input.teamName);
const laneId = String(input.laneId);
const runId = String(input.runId);
await writeCommittedOpenCodeSessionStore({
teamName,
laneId,
runId,
sessions: [
{
id: `oc-session-${memberName}`,
teamName,
memberName,
laneId,
runId,
source: 'runtime_bootstrap_checkin',
},
],
});
return {
runId: String(input.runId),
teamName: String(input.teamName),
runId,
teamName,
launchPhase: 'finished',
teamLaunchState: 'clean_success',
members: {

View file

@ -1834,6 +1834,94 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
expect(rows[0].read).toBe(false);
});
it('keeps accepted OpenCode prompt rows pending without warning when response proof is terminally absent', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
hoisted.files.set(
`/mock/teams/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
seedMemberInbox(teamName, 'jack', [
{
from: 'bob',
to: 'jack',
text: 'Please sync your current task.',
timestamp: '2026-02-23T17:04:00.000Z',
read: false,
messageId: 'opencode-accepted-terminal-empty-1',
actionMode: 'do',
messageKind: 'member_work_sync_nudge',
},
]);
vi.spyOn(service, 'deliverOpenCodeMemberMessage').mockResolvedValue({
delivered: false,
accepted: true,
responsePending: false,
responseState: 'empty_assistant_turn',
ledgerStatus: 'failed_terminal',
ledgerRecordId: 'ledger-1',
laneId: 'secondary:opencode:jack',
reason: 'empty_assistant_turn',
diagnostics: ['empty_assistant_turn'],
});
const relay = await service.relayOpenCodeMemberInboxMessages(teamName, 'jack');
expect(relay).toMatchObject({
relayed: 0,
attempted: 1,
delivered: 0,
failed: 0,
lastDelivery: {
delivered: false,
accepted: true,
responsePending: false,
ledgerStatus: 'failed_terminal',
reason: 'empty_assistant_turn',
},
});
expect(vi.mocked(console.warn)).not.toHaveBeenCalledWith(
expect.stringContaining('OpenCode inbox relay failed')
);
const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]');
expect(rows[0].read).toBe(false);
});
it('does not treat empty OpenCode observations as accepted without delivered prompt proof', () => {
const service = new TeamProvisioningService();
const isAccepted = (
service as unknown as {
isOpenCodePromptAcceptedByObservation: (observation?: unknown) => boolean;
}
).isOpenCodePromptAcceptedByObservation.bind(service);
expect(
isAccepted({
state: 'empty_assistant_turn',
deliveredUserMessageId: null,
})
).toBe(false);
expect(
isAccepted({
state: 'prompt_delivered_no_assistant_message',
deliveredUserMessageId: '',
})
).toBe(false);
expect(
isAccepted({
state: 'empty_assistant_turn',
deliveredUserMessageId: 'opencode-user-message-1',
})
).toBe(true);
});
it('reuses existing OpenCode prompt ledger metadata during watchdog relay retries', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
@ -1901,6 +1989,91 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
);
});
it('ignores stale OpenCode watchdog jobs after the runtime lane is no longer active', async () => {
vi.useFakeTimers();
try {
const service = new TeamProvisioningService();
const teamName = 'my-team';
hoisted.files.set(
`/mock/teams/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
seedMemberInbox(teamName, 'jack', [
{
from: 'bob',
to: 'jack',
text: 'Please sync.',
timestamp: '2026-02-23T17:00:00.000Z',
read: false,
messageId: 'opencode-stale-watchdog-1',
},
]);
const deliverSpy = vi
.spyOn(service, 'deliverOpenCodeMemberMessage')
.mockRejectedValue(
new Error('OpenCode prompt delivery record not found: opencode-prompt:stale')
);
(service as any).scheduleOpenCodePromptDeliveryWatchdog({
teamName,
memberName: 'jack',
messageId: 'opencode-stale-watchdog-1',
delayMs: 500,
});
await vi.advanceTimersByTimeAsync(500);
await Promise.resolve();
expect(deliverSpy).not.toHaveBeenCalled();
expect(vi.mocked(console.warn)).not.toHaveBeenCalled();
} finally {
vi.useRealTimers();
}
});
it('does not classify missing OpenCode watchdog ledger rows as stale while the lane is active', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
attachAliveRun(service, teamName);
hoisted.files.set(
`/mock/teams/${teamName}/config.json`,
JSON.stringify({
name: teamName,
projectPath: '/tmp/my-team',
members: [
{ name: 'team-lead', agentType: 'team-lead' },
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
],
})
);
seedMemberInbox(teamName, 'jack', [
{
from: 'bob',
to: 'jack',
text: 'Please sync.',
timestamp: '2026-02-23T17:00:00.000Z',
read: false,
messageId: 'opencode-active-watchdog-1',
},
]);
vi.spyOn(service as any, 'isOpenCodeRuntimeLaneIndexActive').mockResolvedValue(true);
await expect(
(service as any).isStaleOpenCodePromptDeliveryWatchdogError({
teamName,
memberName: 'jack',
messageId: 'opencode-active-watchdog-1',
error: new Error('OpenCode prompt delivery record not found: opencode-prompt:active'),
})
).resolves.toBe(false);
});
it('skips failed-terminal OpenCode rows without blocking newer unread rows', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';

View file

@ -4,6 +4,7 @@ import * as path from 'path';
import Fastify from 'fastify';
import { buildMemberWorkSyncRuntimeTurnSettledEnvironment } from '../../../../src/features/member-work-sync/main';
import { registerTeamRoutes } from '../../../../src/main/http/teams';
import { applyOpenCodeAutoUpdatePolicy } from '../../../../src/main/services/runtime/openCodeAutoUpdatePolicy';
import { OpenCodeBridgeCommandClient } from '../../../../src/main/services/team/opencode/bridge/OpenCodeBridgeCommandClient';
@ -39,6 +40,7 @@ export interface InboxMessage {
to?: string;
text?: string;
messageId?: string;
messageKind?: string;
read?: boolean;
taskRefs?: TaskRef[];
source?: string;
@ -55,13 +57,17 @@ export async function createOpenCodeLiveHarness(input: {
tempDir: string;
selectedModel: string;
projectPath?: string;
configureServices?: (
svc: TeamProvisioningService
) => Partial<HttpServices> | Promise<Partial<HttpServices> | void> | void;
}): Promise<OpenCodeLiveHarness> {
const orchestratorCli =
process.env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH?.trim() || DEFAULT_ORCHESTRATOR_CLI;
await assertExecutable(orchestratorCli);
const svc = new TeamProvisioningService();
const controlApi = await startLiveTeamControlApi(svc);
const extraServices = (await input.configureServices?.(svc)) ?? {};
const controlApi = await startLiveTeamControlApi(svc, extraServices);
svc.setControlApiBaseUrlResolver(async () => controlApi.baseUrl);
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
@ -75,6 +81,13 @@ export async function createOpenCodeLiveHarness(input: {
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ENTRY: mcpLaunchSpec.args[0] ?? '',
CLAUDE_MULTIMODEL_AGENT_TEAMS_MCP_ARGS_JSON: JSON.stringify(mcpLaunchSpec.args),
};
const turnSettledEnv = await buildMemberWorkSyncRuntimeTurnSettledEnvironment({
teamsBasePath: getTeamsBasePath(),
provider: 'opencode',
});
if (turnSettledEnv) {
Object.assign(bridgeEnv, turnSettledEnv);
}
if (process.env.OPENCODE_E2E_USE_REAL_APP_CREDENTIALS !== '1') {
bridgeEnv.XDG_DATA_HOME = path.join(input.tempDir, 'xdg-data');
} else if (stableBridgeEnv.XDG_DATA_HOME) {
@ -326,13 +339,17 @@ function getTranscriptDurableState(transcript: unknown): string | null {
return typeof durableState === 'string' ? durableState : null;
}
async function startLiveTeamControlApi(svc: TeamProvisioningService): Promise<{
async function startLiveTeamControlApi(
svc: TeamProvisioningService,
extraServices: Partial<HttpServices> = {}
): Promise<{
baseUrl: string;
close: () => Promise<void>;
}> {
const app = Fastify({ logger: false });
registerTeamRoutes(app, {
teamProvisioningService: svc,
...extraServices,
} as HttpServices);
await app.listen({ host: '127.0.0.1', port: 0 });
const address = app.server.address();