From 09004df72cba755f809e9b012bb2e53be4aa6c86 Mon Sep 17 00:00:00 2001 From: 777genius Date: Tue, 21 Apr 2026 21:03:48 +0300 Subject: [PATCH] feat(opencode): add production proof workflow --- package.json | 1 + scripts/prove-opencode-production.mjs | 72 +++++++++++++++++++ src/main/index.ts | 41 +++++------ .../e2e/OpenCodeProductionE2EEvidence.ts | 2 +- .../e2e/OpenCodeProductionE2EEvidencePath.ts | 20 ++++++ .../OpenCodeProductionE2EEvidence.test.ts | 2 +- .../OpenCodeProductionE2EEvidencePath.test.ts | 33 +++++++++ 7 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 scripts/prove-opencode-production.mjs create mode 100644 src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidencePath.ts create mode 100644 test/main/services/team/OpenCodeProductionE2EEvidencePath.test.ts diff --git a/package.json b/package.json index 4901dab5..b1d34e6a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "dev": "node ./scripts/dev-with-runtime.mjs", "dev:web": "node ./scripts/dev-web.mjs", "dev:kill": "node bin/kill-dev.js", + "opencode:prove-production": "node ./scripts/prove-opencode-production.mjs", "prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build", "build": "electron-vite build", "dist": "electron-builder --mac --win --linux", diff --git a/scripts/prove-opencode-production.mjs b/scripts/prove-opencode-production.mjs new file mode 100644 index 00000000..020fba5d --- /dev/null +++ b/scripts/prove-opencode-production.mjs @@ -0,0 +1,72 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process'; +import os from 'node:os'; +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 defaultEvidencePath = path.join( + resolveAppDataDir(), + 'claude-agent-teams-ui', + 'opencode-bridge', + 'production-e2e-evidence.json' +); +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_PROJECT_PATH: process.env.OPENCODE_E2E_PROJECT_PATH?.trim() || repoRoot, + OPENCODE_E2E_MODEL: process.env.OPENCODE_E2E_MODEL?.trim() || 'opencode/big-pickle', + OPENCODE_E2E_WRITE_APP_EVIDENCE: '1', + OPENCODE_E2E_WRITE_EVIDENCE_PATH: + process.env.OPENCODE_E2E_WRITE_EVIDENCE_PATH?.trim() || + process.env.CLAUDE_TEAM_OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH?.trim() || + defaultEvidencePath, + 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 production proof'); +console.log(`Model: ${env.OPENCODE_E2E_MODEL}`); +console.log(`Project: ${env.OPENCODE_E2E_PROJECT_PATH}`); +console.log(`Evidence: ${env.OPENCODE_E2E_WRITE_EVIDENCE_PATH}`); +console.log(`Orchestrator CLI: ${env.CLAUDE_AGENT_TEAMS_ORCHESTRATOR_CLI_PATH}`); + +const result = spawnSync( + 'pnpm', + ['exec', 'vitest', 'run', 'test/main/services/team/OpenCodeProductionGate.live.test.ts'], + { + cwd: repoRoot, + env, + stdio: 'inherit', + shell: process.platform === 'win32', + } +); + +if (result.error) { + console.error(`Failed to run OpenCode production proof: ${result.error.message}`); + process.exit(1); +} + +process.exit(result.status ?? 1); + +function resolveAppDataDir() { + if (process.platform === 'darwin') { + return path.join(os.homedir(), 'Library', 'Application Support'); + } + + if (process.platform === 'win32') { + return process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); + } + + return process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'); +} diff --git a/src/main/index.ts b/src/main/index.ts index 8fa27bdb..4de501e4 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -20,14 +20,14 @@ process.env.UV_THREADPOOL_SIZE ??= '16'; import './sentry'; import { - createCodexAccountFeature, type CodexAccountFeatureFacade, + createCodexAccountFeature, registerCodexAccountIpc, removeCodexAccountIpc, } from '@features/codex-account/main'; import { - createCodexModelCatalogFeature, type CodexModelCatalogFeatureFacade, + createCodexModelCatalogFeature, } from '@features/codex-model-catalog/main'; import { createRecentProjectsFeature, @@ -35,6 +35,7 @@ import { registerRecentProjectsIpc, removeRecentProjectsIpc, } from '@features/recent-projects/main'; +import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService'; import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository'; import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor'; import { SchedulerService } from '@main/services/schedule/SchedulerService'; @@ -52,7 +53,6 @@ import { TeamMcpConfigBuilder, } from '@main/services/team/TeamMcpConfigBuilder'; import { resolveInteractiveShellEnv } from '@main/utils/shellEnv'; -import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService'; import { CONTEXT_CHANGED, SCHEDULE_CHANGE, @@ -103,6 +103,19 @@ import { import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor'; import { HttpServer } from './services/infrastructure/HttpServer'; import { clearAutoResumeService } from './services/team/AutoResumeService'; +import { OpenCodeBridgeCommandClient } from './services/team/opencode/bridge/OpenCodeBridgeCommandClient'; +import { + createOpenCodeBridgeCommandLeaseStore, + createOpenCodeBridgeCommandLedgerStore, +} from './services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore'; +import { + createOpenCodeBridgeClientIdentity, + OpenCodeBridgeCommandHandshakePort, +} from './services/team/opencode/bridge/OpenCodeBridgeHandshakeClient'; +import { OpenCodeStateChangingBridgeCommandService } from './services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService'; +import { resolveOpenCodeProductionE2EEvidencePath } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidencePath'; +import { OpenCodeProductionE2EEvidenceStore } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore'; +import { OpenCodeRuntimeManifestEvidenceReader } from './services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader'; import { buildTeamControlApiBaseUrl, clearTeamControlApiState, @@ -115,18 +128,6 @@ import { type TeamReconcileTrigger, } from './services/team/TeamReconcileDrainScheduler'; import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore'; -import { OpenCodeBridgeCommandClient } from './services/team/opencode/bridge/OpenCodeBridgeCommandClient'; -import { - createOpenCodeBridgeCommandLeaseStore, - createOpenCodeBridgeCommandLedgerStore, -} from './services/team/opencode/bridge/OpenCodeBridgeCommandLedgerStore'; -import { - createOpenCodeBridgeClientIdentity, - OpenCodeBridgeCommandHandshakePort, -} from './services/team/opencode/bridge/OpenCodeBridgeHandshakeClient'; -import { OpenCodeStateChangingBridgeCommandService } from './services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService'; -import { OpenCodeProductionE2EEvidenceStore } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore'; -import { OpenCodeRuntimeManifestEvidenceReader } from './services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader'; import { getAppIconPath } from './utils/appIcon'; import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder'; import { @@ -160,21 +161,21 @@ import { TaskBoundaryParser, TeamDataService, TeamLogSourceTracker, + TeammateToolTracker, + TeamMemberLogsFinder, + TeamProvisioningService, TeamRuntimeAdapterRegistry, TeamTaskStallJournal, TeamTaskStallMonitor, TeamTaskStallNotifier, TeamTaskStallPolicy, TeamTaskStallSnapshotSource, - TeammateToolTracker, - TeamMemberLogsFinder, - TeamProvisioningService, UpdaterService, } from './services'; +import type { OpenCodeTeamLaunchMode } from './services/team'; import type { FileChangeEvent } from '@main/types'; import type { TeamChangeEvent } from '@shared/types'; -import type { OpenCodeTeamLaunchMode } from './services/team'; const logger = createLogger('App'); startEventLoopLagMonitor(); @@ -266,7 +267,7 @@ async function createOpenCodeRuntimeAdapterRegistry(): Promise { 'OpenCode production E2E evidence is expired', 'OpenCode production E2E evidence is missing signals: stale_run_rejected', 'OpenCode production E2E evidence is missing observed MCP tools: agent-teams_runtime_deliver_message', - 'OpenCode production E2E evidence model openrouter/anthropic/claude-sonnet-4.5 does not match selected model openai/gpt-5.4-mini', + 'OpenCode production E2E evidence model openrouter/anthropic/claude-sonnet-4.5 does not match selected model openai/gpt-5.4-mini. Production launch is intentionally scoped to the exact raw model id; regenerate evidence with OPENCODE_E2E_MODEL=openai/gpt-5.4-mini.', ]), }); }); diff --git a/test/main/services/team/OpenCodeProductionE2EEvidencePath.test.ts b/test/main/services/team/OpenCodeProductionE2EEvidencePath.test.ts new file mode 100644 index 00000000..999335b9 --- /dev/null +++ b/test/main/services/team/OpenCodeProductionE2EEvidencePath.test.ts @@ -0,0 +1,33 @@ +import * as path from 'path'; + +import { describe, expect, it } from 'vitest'; + +import { + OPENCODE_PRODUCTION_E2E_EVIDENCE_FILE, + OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH_ENV, + resolveOpenCodeProductionE2EEvidencePath, +} from '../../../../src/main/services/team/opencode/e2e/OpenCodeProductionE2EEvidencePath'; + +describe('OpenCodeProductionE2EEvidencePath', () => { + it('defaults to the app-owned bridge control directory', () => { + expect( + resolveOpenCodeProductionE2EEvidencePath({ + bridgeControlDir: '/app/user-data/opencode-bridge', + env: {}, + }) + ).toBe(path.join('/app/user-data/opencode-bridge', OPENCODE_PRODUCTION_E2E_EVIDENCE_FILE)); + }); + + it('allows release and local proof runs to point production at an explicit artifact', () => { + const relativeOverride = 'tmp/opencode-production-evidence.json'; + + expect( + resolveOpenCodeProductionE2EEvidencePath({ + bridgeControlDir: '/app/user-data/opencode-bridge', + env: { + [OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH_ENV]: ` ${relativeOverride} `, + }, + }) + ).toBe(path.resolve(relativeOverride)); + }); +});