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": "node ./scripts/dev-with-runtime.mjs",
|
||||||
"dev:web": "node ./scripts/dev-web.mjs",
|
"dev:web": "node ./scripts/dev-web.mjs",
|
||||||
"dev:kill": "node bin/kill-dev.js",
|
"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",
|
"prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
|
||||||
"build": "electron-vite build",
|
"build": "electron-vite build",
|
||||||
"dist": "electron-builder --mac --win --linux",
|
"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 './sentry';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createCodexAccountFeature,
|
|
||||||
type CodexAccountFeatureFacade,
|
type CodexAccountFeatureFacade,
|
||||||
|
createCodexAccountFeature,
|
||||||
registerCodexAccountIpc,
|
registerCodexAccountIpc,
|
||||||
removeCodexAccountIpc,
|
removeCodexAccountIpc,
|
||||||
} from '@features/codex-account/main';
|
} from '@features/codex-account/main';
|
||||||
import {
|
import {
|
||||||
createCodexModelCatalogFeature,
|
|
||||||
type CodexModelCatalogFeatureFacade,
|
type CodexModelCatalogFeatureFacade,
|
||||||
|
createCodexModelCatalogFeature,
|
||||||
} from '@features/codex-model-catalog/main';
|
} from '@features/codex-model-catalog/main';
|
||||||
import {
|
import {
|
||||||
createRecentProjectsFeature,
|
createRecentProjectsFeature,
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
registerRecentProjectsIpc,
|
registerRecentProjectsIpc,
|
||||||
removeRecentProjectsIpc,
|
removeRecentProjectsIpc,
|
||||||
} from '@features/recent-projects/main';
|
} from '@features/recent-projects/main';
|
||||||
|
import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService';
|
||||||
import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository';
|
import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository';
|
||||||
import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor';
|
import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor';
|
||||||
import { SchedulerService } from '@main/services/schedule/SchedulerService';
|
import { SchedulerService } from '@main/services/schedule/SchedulerService';
|
||||||
|
|
@ -52,7 +53,6 @@ import {
|
||||||
TeamMcpConfigBuilder,
|
TeamMcpConfigBuilder,
|
||||||
} from '@main/services/team/TeamMcpConfigBuilder';
|
} from '@main/services/team/TeamMcpConfigBuilder';
|
||||||
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
import { resolveInteractiveShellEnv } from '@main/utils/shellEnv';
|
||||||
import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService';
|
|
||||||
import {
|
import {
|
||||||
CONTEXT_CHANGED,
|
CONTEXT_CHANGED,
|
||||||
SCHEDULE_CHANGE,
|
SCHEDULE_CHANGE,
|
||||||
|
|
@ -103,6 +103,19 @@ import {
|
||||||
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
|
import { startEventLoopLagMonitor } from './services/infrastructure/EventLoopLagMonitor';
|
||||||
import { HttpServer } from './services/infrastructure/HttpServer';
|
import { HttpServer } from './services/infrastructure/HttpServer';
|
||||||
import { clearAutoResumeService } from './services/team/AutoResumeService';
|
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 {
|
import {
|
||||||
buildTeamControlApiBaseUrl,
|
buildTeamControlApiBaseUrl,
|
||||||
clearTeamControlApiState,
|
clearTeamControlApiState,
|
||||||
|
|
@ -115,18 +128,6 @@ import {
|
||||||
type TeamReconcileTrigger,
|
type TeamReconcileTrigger,
|
||||||
} from './services/team/TeamReconcileDrainScheduler';
|
} from './services/team/TeamReconcileDrainScheduler';
|
||||||
import { TeamSentMessagesStore } from './services/team/TeamSentMessagesStore';
|
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 { getAppIconPath } from './utils/appIcon';
|
||||||
import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder';
|
import { getProjectsBasePath, getTeamsBasePath, getTodosBasePath } from './utils/pathDecoder';
|
||||||
import {
|
import {
|
||||||
|
|
@ -160,21 +161,21 @@ import {
|
||||||
TaskBoundaryParser,
|
TaskBoundaryParser,
|
||||||
TeamDataService,
|
TeamDataService,
|
||||||
TeamLogSourceTracker,
|
TeamLogSourceTracker,
|
||||||
|
TeammateToolTracker,
|
||||||
|
TeamMemberLogsFinder,
|
||||||
|
TeamProvisioningService,
|
||||||
TeamRuntimeAdapterRegistry,
|
TeamRuntimeAdapterRegistry,
|
||||||
TeamTaskStallJournal,
|
TeamTaskStallJournal,
|
||||||
TeamTaskStallMonitor,
|
TeamTaskStallMonitor,
|
||||||
TeamTaskStallNotifier,
|
TeamTaskStallNotifier,
|
||||||
TeamTaskStallPolicy,
|
TeamTaskStallPolicy,
|
||||||
TeamTaskStallSnapshotSource,
|
TeamTaskStallSnapshotSource,
|
||||||
TeammateToolTracker,
|
|
||||||
TeamMemberLogsFinder,
|
|
||||||
TeamProvisioningService,
|
|
||||||
UpdaterService,
|
UpdaterService,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
|
import type { OpenCodeTeamLaunchMode } from './services/team';
|
||||||
import type { FileChangeEvent } from '@main/types';
|
import type { FileChangeEvent } from '@main/types';
|
||||||
import type { TeamChangeEvent } from '@shared/types';
|
import type { TeamChangeEvent } from '@shared/types';
|
||||||
import type { OpenCodeTeamLaunchMode } from './services/team';
|
|
||||||
|
|
||||||
const logger = createLogger('App');
|
const logger = createLogger('App');
|
||||||
startEventLoopLagMonitor();
|
startEventLoopLagMonitor();
|
||||||
|
|
@ -266,7 +267,7 @@ async function createOpenCodeRuntimeAdapterRegistry(): Promise<TeamRuntimeAdapte
|
||||||
new OpenCodeReadinessBridge(bridgeClient, {
|
new OpenCodeReadinessBridge(bridgeClient, {
|
||||||
stateChangingCommands,
|
stateChangingCommands,
|
||||||
productionE2eEvidence: new OpenCodeProductionE2EEvidenceStore({
|
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');
|
diagnostics.push('OpenCode production gate cannot verify selected raw model id');
|
||||||
} else if (evidence.selectedModel !== expected.selectedModel) {
|
} else if (evidence.selectedModel !== expected.selectedModel) {
|
||||||
diagnostics.push(
|
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 expired',
|
||||||
'OpenCode production E2E evidence is missing signals: stale_run_rejected',
|
'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 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