feat(opencode): add production proof workflow

This commit is contained in:
777genius 2026-04-21 21:03:48 +03:00
parent 62cded28cc
commit 09004df72c
7 changed files with 149 additions and 22 deletions

View file

@ -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",

View file

@ -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');
}

View file

@ -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<TeamRuntimeAdapte
new OpenCodeReadinessBridge(bridgeClient, {
stateChangingCommands,
productionE2eEvidence: new OpenCodeProductionE2EEvidenceStore({
filePath: join(bridgeControlDir, 'production-e2e-evidence.json'),
filePath: resolveOpenCodeProductionE2EEvidencePath({ bridgeControlDir }),
}),
}),
{

View file

@ -325,7 +325,7 @@ function collectExpectedRuntimeDiagnostics(
diagnostics.push('OpenCode production gate cannot verify selected raw model id');
} else if (evidence.selectedModel !== expected.selectedModel) {
diagnostics.push(
`OpenCode production E2E evidence model ${evidence.selectedModel} does not match selected model ${expected.selectedModel}`
`OpenCode production E2E evidence model ${evidence.selectedModel} does not match selected model ${expected.selectedModel}. Production launch is intentionally scoped to the exact raw model id; regenerate evidence with OPENCODE_E2E_MODEL=${expected.selectedModel}.`
);
}

View file

@ -0,0 +1,20 @@
import { join, resolve } from 'path';
export const OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH_ENV =
'CLAUDE_TEAM_OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH';
export const OPENCODE_PRODUCTION_E2E_EVIDENCE_FILE = 'production-e2e-evidence.json';
export function resolveOpenCodeProductionE2EEvidencePath(input: {
bridgeControlDir: string;
env?: NodeJS.ProcessEnv;
}): string {
const env = input.env ?? process.env;
const overridePath = env[OPENCODE_PRODUCTION_E2E_EVIDENCE_PATH_ENV]?.trim();
if (overridePath) {
return resolve(overridePath);
}
return join(input.bridgeControlDir, OPENCODE_PRODUCTION_E2E_EVIDENCE_FILE);
}

View file

@ -82,7 +82,7 @@ describe('OpenCodeProductionE2EEvidence', () => {
'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.',
]),
});
});

View file

@ -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));
});
});