feat(opencode): add production proof workflow
This commit is contained in:
parent
62cded28cc
commit
09004df72c
7 changed files with 149 additions and 22 deletions
|
|
@ -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",
|
||||
|
|
|
|||
72
scripts/prove-opencode-production.mjs
Normal file
72
scripts/prove-opencode-production.mjs
Normal 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');
|
||||
}
|
||||
|
|
@ -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 }),
|
||||
}),
|
||||
}),
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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}.`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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.',
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue