testronaut 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +198 -0
  2. package/TODO.md +5 -0
  3. package/bin/cli.js +215 -2
  4. package/core/agent.js +1 -0
  5. package/core/browserEvidence.js +184 -0
  6. package/core/jevRouting.js +77 -0
  7. package/core/jevShadow.js +325 -0
  8. package/core/jevSuiteSelection.js +39 -0
  9. package/core/jevUsage.js +62 -0
  10. package/core/optimization.js +98 -0
  11. package/core/turnLoop.js +174 -10
  12. package/experiments/CROSS_PROVIDER.md +19 -0
  13. package/experiments/README.md +62 -0
  14. package/experiments/buildExperimentPublicationPackage.js +219 -0
  15. package/experiments/buildOptimizationDataPackage.js +390 -0
  16. package/experiments/crossProviderBenchmark.js +227 -0
  17. package/experiments/optimizationBenchmark.js +339 -0
  18. package/package.json +5 -1
  19. package/tests/binTests/cli.helpers.test.js +16 -0
  20. package/tests/binTests/cli.integration.test.js +14 -0
  21. package/tests/coreTests/browserEvidence.test.js +70 -0
  22. package/tests/coreTests/jevRouting.test.js +57 -0
  23. package/tests/coreTests/jevShadow.test.js +158 -0
  24. package/tests/coreTests/jevSuiteSelection.test.js +24 -0
  25. package/tests/coreTests/jevUsage.test.js +58 -0
  26. package/tests/coreTests/optimization.test.js +80 -0
  27. package/tests/coreTests/turnLoop.test.js +304 -0
  28. package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
  29. package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
  30. package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
  31. package/tests/toolsTests/generateHtmlReport.test.js +80 -0
  32. package/tools/generateHtmlReport.js +57 -5
@@ -0,0 +1,98 @@
1
+ export const OPTIMIZATION_OBJECTIVES = ['off', 'cost', 'tokens', 'balanced', 'speed'];
2
+ export const OPTIMIZATION_MODES = ['off', 'shadow', 'live'];
3
+
4
+ const OBJECTIVE_ALIASES = {
5
+ neither: 'off',
6
+ 'lower-spend': 'cost',
7
+ spend: 'cost',
8
+ 'fewer-tokens': 'tokens',
9
+ both: 'balanced',
10
+ latency: 'speed',
11
+ };
12
+
13
+ function normalize(value, allowed, aliases = {}) {
14
+ const normalized = String(value ?? '').trim().toLowerCase();
15
+ const resolved = aliases[normalized] || normalized;
16
+ return allowed.includes(resolved) ? resolved : null;
17
+ }
18
+
19
+ function truthy(value) {
20
+ return /^(1|true|yes|on)$/i.test(String(value ?? '').trim());
21
+ }
22
+
23
+ export function resolveOptimizationPolicy({ env = process.env, config = {} } = {}) {
24
+ const configValue = config.optimization || {};
25
+ const guardrailsValue = config.guardrails || {};
26
+ const guardrailsConfigured = guardrailsValue.enabled !== undefined || guardrailsValue.mode !== undefined;
27
+ const guardrailsEnabled = guardrailsValue.enabled !== false && guardrailsValue.mode !== 'off';
28
+ const rawObjective = env.TESTRONAUT_OPTIMIZE
29
+ ?? configValue.objective
30
+ ?? (guardrailsConfigured ? (guardrailsEnabled ? 'tokens' : 'off') : undefined);
31
+ const rawMode = env.TESTRONAUT_OPTIMIZATION_MODE
32
+ ?? configValue.mode
33
+ ?? (guardrailsConfigured ? (guardrailsEnabled ? (guardrailsValue.mode || 'live') : 'off') : undefined);
34
+ const objective = normalize(rawObjective, OPTIMIZATION_OBJECTIVES, OBJECTIVE_ALIASES);
35
+ const mode = normalize(rawMode, OPTIMIZATION_MODES);
36
+ const configured = rawObjective !== undefined || rawMode !== undefined;
37
+ const effectiveObjective = objective ?? (configured ? 'off' : null);
38
+ const effectiveMode = mode ?? (configured && effectiveObjective !== 'off' ? 'shadow' : 'off');
39
+ const routingRequested = ['cost', 'balanced', 'speed'].includes(effectiveObjective);
40
+ const betaModelRouting = truthy(
41
+ env.TESTRONAUT_BETA_MODEL_ROUTING ?? configValue.beta?.modelRouting,
42
+ );
43
+ const threshold = Number(
44
+ env.TESTRONAUT_JEV_ROUTING_THRESHOLD
45
+ ?? configValue.guardrails?.minimumRoutingProbability,
46
+ );
47
+
48
+ return {
49
+ configured,
50
+ objective: effectiveObjective,
51
+ mode: effectiveObjective === 'off' ? 'off' : effectiveMode,
52
+ routingRequested,
53
+ betaModelRouting,
54
+ routing: routingRequested && betaModelRouting,
55
+ tokenReduction: ['tokens', 'balanced'].includes(effectiveObjective),
56
+ completionGate: effectiveMode === 'live' && effectiveObjective !== 'off',
57
+ routingProbability: Number.isFinite(threshold) && threshold >= 0 && threshold <= 1
58
+ ? threshold
59
+ : 0.4,
60
+ completionProbability: (() => {
61
+ const value = Number(
62
+ env.TESTRONAUT_JEV_GATE_THRESHOLD
63
+ ?? guardrailsValue.completionProbability
64
+ ?? configValue.guardrails?.completionProbability,
65
+ );
66
+ return Number.isFinite(value) && value >= 0 && value <= 1 ? value : 0.8;
67
+ })(),
68
+ valid: (!configured || Boolean(objective || rawObjective === undefined))
69
+ && (rawMode === undefined || Boolean(mode)),
70
+ };
71
+ }
72
+
73
+ export function applyOptimizationPolicy(policy, env = process.env, { force = false } = {}) {
74
+ if (!policy?.configured) return env;
75
+ env.TESTRONAUT_OPTIMIZATION_OBJECTIVE_EFFECTIVE = policy.objective;
76
+ env.TESTRONAUT_OPTIMIZATION_MODE_EFFECTIVE = policy.mode;
77
+ if (force || env.TESTRONAUT_JEV_ROUTING_THRESHOLD === undefined) {
78
+ env.TESTRONAUT_JEV_ROUTING_THRESHOLD = String(policy.routingProbability);
79
+ }
80
+ if (force || env.TESTRONAUT_JEV_GATE_THRESHOLD === undefined) {
81
+ env.TESTRONAUT_JEV_GATE_THRESHOLD = String(policy.completionProbability);
82
+ }
83
+ if (force || env.TESTRONAUT_JEV_ROUTING === undefined) {
84
+ env.TESTRONAUT_JEV_ROUTING = policy.routing ? policy.mode : 'off';
85
+ }
86
+ if (force || env.TESTRONAUT_JEV_GATE === undefined) {
87
+ env.TESTRONAUT_JEV_GATE = policy.completionGate ? '1' : '0';
88
+ }
89
+ if (force || env.TESTRONAUT_JEV_STRATEGIES === undefined) {
90
+ env.TESTRONAUT_JEV_STRATEGIES = policy.routing ? '1' : '0';
91
+ }
92
+ if (force || env.TESTRONAUT_JEV_SHADOW === undefined) {
93
+ env.TESTRONAUT_JEV_SHADOW = policy.mode === 'off' ? '0' : '1';
94
+ }
95
+ return env;
96
+ }
97
+
98
+ export const __optimizationInternals = { normalize, OBJECTIVE_ALIASES };
package/core/turnLoop.js CHANGED
@@ -49,7 +49,10 @@ import {
49
49
  import { resolveProviderModel } from '../llm/modelResolver.js';
50
50
  import { getLLM } from '../llm/llmFactory.js';
51
51
  import { summarizeTurnIntentFromMessage } from './turnIntent.js';
52
- import { maskPreview, redactArgs } from './redaction.js';
52
+ import { maskPreview, redactArgs, redactPasswordInText } from './redaction.js';
53
+ import { buildJevShadowState, buildJevStrategyQuestions, evaluateJevCompletionCandidate, evaluateJevShadow, extractJevStrategyResults, resolveJevShadowConfig, sanitizeJevShadowEvents } from './jevShadow.js';
54
+ import { buildBrowserEvidence, collectBrowserControlState, mergeBrowserControlState } from './browserEvidence.js';
55
+ import { chooseJevTurnModel, resolveJevRoutingConfig } from './jevRouting.js';
53
56
  import {
54
57
  sanitizeHeavyToolHistory,
55
58
  pruneConversationContext,
@@ -169,6 +172,20 @@ const FIRE_AND_FORGET_TOOLS = new Set([
169
172
  'record_mission_telemetry',
170
173
  ]);
171
174
 
175
+ function currentBrowserUrl(browser) {
176
+ try { return browser?.page?.url?.() || null; } catch { return null; }
177
+ }
178
+
179
+ async function captureBrowserEvidence(browser, html, previous) {
180
+ const evidence = buildBrowserEvidence(html, {
181
+ url: currentBrowserUrl(browser),
182
+ previous,
183
+ redactText: redactPasswordInText,
184
+ });
185
+ const states = await collectBrowserControlState(browser);
186
+ return mergeBrowserControlState(evidence, states);
187
+ }
188
+
172
189
  /**
173
190
  * Utility: format byte counts into human-readable strings.
174
191
  * Used when reporting upload/download events in mission logs.
@@ -221,6 +238,11 @@ const pushDOMAssistant = async (browser, messages, agentMemory, { skipIfLastTool
221
238
  agentMemory
222
239
  );
223
240
  await tokenEstimate(MODEL_ID, domHtml);
241
+ agentMemory.browserEvidence = await captureBrowserEvidence(
242
+ browser,
243
+ domHtml,
244
+ agentMemory.browserEvidence,
245
+ );
224
246
 
225
247
  // Push the corresponding tool response back into the conversation
226
248
  messages.push({
@@ -269,6 +291,11 @@ export const turnLoop = async (
269
291
  dataTypes: ['document', 'file', 'item', 'row'],
270
292
  };
271
293
  const stepsArchive = ctx.stepsArchive || steps;
294
+ const jevShadowConfig = resolveJevShadowConfig();
295
+ const jevRoutingConfig = resolveJevRoutingConfig(process.env, {
296
+ provider: PROVIDER_ID,
297
+ primaryModel: MODEL_ID,
298
+ });
272
299
  const retryLimitClamped = Math.min(10, Math.max(1, Number.isFinite(retryLimitRaw) ? retryLimitRaw : DEFAULT_TURN_RETRY_LIMIT)); // retries allowed (excludes initial)
273
300
  const maxAttempts = retryLimitClamped + 1; // includes initial attempt
274
301
  ctx.groundControl = groundControl;
@@ -281,6 +308,102 @@ export const turnLoop = async (
281
308
  ensureDocProgress(agentMemory, resourceGuardCfg);
282
309
  let turnRetries = 0;
283
310
  let stepSeq = ctx._stepSeq || 0;
311
+ let nextJevStrategy = null;
312
+ let forcePrimaryRetry = false;
313
+
314
+ const recordJevShadow = async (step, msg, phase, proposedTools = []) => {
315
+ if (!jevShadowConfig.enabled) return;
316
+ nextJevStrategy = null;
317
+ // Keep status lines and purpose-built telemetry, but exclude raw tool payload
318
+ // lines (which may contain DOM, page text, or other third-party data).
319
+ const safeEvents = sanitizeJevShadowEvents(step.events, redactPasswordInText);
320
+ agentMemory.browserEvidence = await captureBrowserEvidence(
321
+ browser,
322
+ null,
323
+ agentMemory.browserEvidence,
324
+ );
325
+ const state = buildJevShadowState({
326
+ mission: redactPasswordInText(ctx.goal || missionName),
327
+ turn: step.turn,
328
+ maxTurns,
329
+ phase,
330
+ assistantSummary: redactPasswordInText(step.summary || ''),
331
+ assistantContent: redactPasswordInText(msg?.content || ''),
332
+ proposedTools,
333
+ events: safeEvents,
334
+ groundControl,
335
+ browserEvidence: agentMemory.browserEvidence,
336
+ });
337
+ const strategyCatalog = jevShadowConfig.strategiesEnabled
338
+ ? buildJevStrategyQuestions(agentMemory.browserEvidence)
339
+ : null;
340
+ step.jevShadow = await evaluateJevShadow(state, {
341
+ config: jevShadowConfig,
342
+ ...(strategyCatalog ? { questions: strategyCatalog.questions } : {}),
343
+ });
344
+ if (step.jevShadow.status === 'ok' && strategyCatalog) {
345
+ step.jevShadow.strategies = extractJevStrategyResults(
346
+ step.jevShadow.answers,
347
+ strategyCatalog,
348
+ );
349
+ nextJevStrategy = step.jevShadow.strategies;
350
+ }
351
+ step.jevShadow.completionGate = evaluateJevCompletionCandidate(step.jevShadow, {
352
+ threshold: jevShadowConfig.gateThreshold,
353
+ browserEvidence: agentMemory.browserEvidence,
354
+ });
355
+ const status = step.jevShadow?.status || 'unknown';
356
+ const latency = Number.isFinite(step.jevShadow?.latencyMs)
357
+ ? ` in ${step.jevShadow.latencyMs}ms`
358
+ : '';
359
+ console.log(`🔬 Jev shadow: ${status}${latency}`);
360
+ };
361
+
362
+ const finalizeWithJevGate = async (step) => {
363
+ if (!jevShadowConfig.gateEnabled || !step.jevShadow?.completionGate?.candidate) return null;
364
+
365
+ const prog = ensureDocProgress(agentMemory, resourceGuardCfg);
366
+ if (prog?.items.length && prog.downloaded.size < prog.items.length) {
367
+ step.jevGate = { triggered: false, reason: 'resource_guard_incomplete' };
368
+ return null;
369
+ }
370
+
371
+ const existingScreenshot = Boolean(
372
+ step.screenshotPath
373
+ || steps.some(existing => existing?.screenshotPath)
374
+ || stepsArchive.some(existing => existing?.screenshotPath),
375
+ );
376
+ if (process.env.TESTRONAUT_SCREENSHOTS !== '0' && !existingScreenshot) {
377
+ try {
378
+ const screenshotResult = await CHROME_TOOL_MAP.screenshot(
379
+ browser,
380
+ { name: `jev-completion-turn-${step.turn + 1}` },
381
+ agentMemory,
382
+ );
383
+ const match = String(screenshotResult).match(/screenshot.*?saved at: (.+\.png)/i);
384
+ if (match?.[1]) step.screenshotPath = match[1];
385
+ step.events.push('🖼️ Jev completion gate captured final screenshot');
386
+ } catch (error) {
387
+ step.jevGate = { triggered: false, reason: 'screenshot_failed', error: error?.message || String(error) };
388
+ step.events.push(`⚠️ Jev completion gate screenshot failed: ${error?.message || error}`);
389
+ return null;
390
+ }
391
+ }
392
+
393
+ const gate = step.jevShadow.completionGate;
394
+ recordGroundTelemetry(groundControl, {
395
+ kind: 'assertion',
396
+ text: `Jev completion gate satisfied at ${Math.round(gate.threshold * 100)}% threshold.`,
397
+ status: 'passed',
398
+ }, { turn: step.turn, source: 'jev_completion_gate', signals: gate.signals });
399
+
400
+ step.jevGate = { triggered: true, ...gate };
401
+ step.events.push(`🔬 Jev completion gate triggered at ${Math.round(gate.threshold * 100)}% threshold`);
402
+ step.result = '✅ Mission Success (Jev gate)';
403
+ recordStep(step);
404
+ const finalMessage = 'SUCCESS: Jev completion gate found sufficient browser evidence.';
405
+ return { success: true, finalMessage, steps: stepsArchive, completedBy: 'jev_gate' };
406
+ };
284
407
 
285
408
  // Centralized recorder: push to in-memory buffer AND fire optional callback for streaming
286
409
  // Idempotent recorder – prevents accidental double-push
@@ -310,6 +433,20 @@ export const turnLoop = async (
310
433
  result: '🟡 In Progress',
311
434
  missionName,
312
435
  };
436
+ const modelRouting = chooseJevTurnModel(nextJevStrategy, jevRoutingConfig, {
437
+ provider: PROVIDER_ID,
438
+ primaryModel: MODEL_ID,
439
+ });
440
+ if (forcePrimaryRetry) {
441
+ modelRouting.selectedModel = MODEL_ID;
442
+ modelRouting.applied = false;
443
+ modelRouting.reason = 'routed_retry_primary';
444
+ modelRouting.forcedPrimaryRetry = true;
445
+ forcePrimaryRetry = false;
446
+ }
447
+ step.modelRouting = modelRouting;
448
+ step.provider = PROVIDER_ID;
449
+ step.model = modelRouting.selectedModel;
313
450
  if (attempt > 1) {
314
451
  const retryNumber = attempt - 1;
315
452
  step.events.push(`🔁 Re-attempt ${retryNumber}/${retryLimitClamped} for turn`);
@@ -372,13 +509,14 @@ export const turnLoop = async (
372
509
  // ─────────────────────────────────────────────
373
510
  // STEP 2: Request next reasoning turn from model
374
511
  // ─────────────────────────────────────────────
375
- const contextCheck = await warnIfContextNearLimit(MODEL_ID, { messages, tools: activeToolsSchema });
512
+ const turnModel = modelRouting.selectedModel;
513
+ const contextCheck = await warnIfContextNearLimit(turnModel, { messages, tools: activeToolsSchema });
376
514
  const projectedTokens = contextCheck.estimatedTokens
377
- ?? await tokenEstimate(MODEL_ID, { messages, tools: activeToolsSchema });
515
+ ?? await tokenEstimate(turnModel, { messages, tools: activeToolsSchema });
378
516
  ({ totalTokensUsed, turnTimestamps, shouldBackoff } = await tokenUseCoolOff(
379
517
  totalTokensUsed,
380
518
  turnTimestamps,
381
- MODEL_ID,
519
+ turnModel,
382
520
  projectedTokens
383
521
  ));
384
522
  if (shouldBackoff) {
@@ -386,13 +524,21 @@ export const turnLoop = async (
386
524
  turn -= 1;
387
525
  continue;
388
526
  }
389
- const { message, usage, headers } = await llm.chat({
390
- model: MODEL_ID,
391
- messages,
392
- tools: activeToolsSchema,
393
- });
527
+ let modelResponse;
528
+ try {
529
+ modelResponse = await llm.chat({ model: turnModel, messages, tools: activeToolsSchema });
530
+ } catch (routingError) {
531
+ if (!modelRouting.applied) throw routingError;
532
+ modelRouting.fallback = true;
533
+ modelRouting.fallbackReason = routingError?.message || String(routingError);
534
+ modelRouting.selectedModel = MODEL_ID;
535
+ step.model = MODEL_ID;
536
+ step.events.push(`↩️ Jev route fallback to ${MODEL_ID}`);
537
+ modelResponse = await llm.chat({ model: MODEL_ID, messages, tools: activeToolsSchema });
538
+ }
539
+ const { message, usage, headers } = modelResponse;
394
540
  response = { message, usage, headers };
395
- updateLimitsFromHeaders(MODEL_ID, headers);
541
+ updateLimitsFromHeaders(modelRouting.selectedModel, headers);
396
542
 
397
543
  } catch (err) {
398
544
  // ─────────────────────────────────────────────
@@ -460,6 +606,7 @@ export const turnLoop = async (
460
606
  if (msg.tool_calls?.length) {
461
607
  console.log('Processing tool calls...');
462
608
  const toolResponses = [];
609
+ const proposedTools = [];
463
610
  let hadToolIssues = false;
464
611
  let domRefreshSource = null;
465
612
 
@@ -467,6 +614,7 @@ export const turnLoop = async (
467
614
  const fnName = call.function.name;
468
615
  const args = JSON.parse(call.function.arguments || '{}');
469
616
  const safeArgs = redactArgs(fnName, args);
617
+ proposedTools.push({ name: fnName, arguments: safeArgs });
470
618
  console.log(`[model] → ${fnName}`, safeArgs);
471
619
  step.events.push(`[model] → ${fnName} ${JSON.stringify(safeArgs)}`);
472
620
 
@@ -761,6 +909,14 @@ export const turnLoop = async (
761
909
  }
762
910
  }
763
911
 
912
+ if (!errorMessage && fnName === 'get_dom') {
913
+ agentMemory.browserEvidence = await captureBrowserEvidence(
914
+ browser,
915
+ result,
916
+ agentMemory.browserEvidence,
917
+ );
918
+ }
919
+
764
920
  // Decide what to send back to the LLM for this tool.
765
921
  // - For "fire-and-forget" tools, send a tiny stub (OK/error) to avoid
766
922
  // bloating context with large payloads (file JSON, etc.).
@@ -833,11 +989,17 @@ export const turnLoop = async (
833
989
  step.events.push(`[auto] → DOM size after ${domRefreshSource}: ${domRefresh.domLength} chars`);
834
990
  }
835
991
  }
992
+ await recordJevShadow(step, msg, hadToolIssues ? 'after_action_error' : 'after_action', proposedTools);
993
+ if (!hadToolIssues) {
994
+ const gatedResult = await finalizeWithJevGate(step);
995
+ if (gatedResult) return gatedResult;
996
+ }
836
997
  if (hadToolIssues && turnRetries < retryLimitClamped) {
837
998
  step.result = '⏳ Retrying turn';
838
999
  const retryNumber = attempt - 1;
839
1000
  step.events.push(`🔁 Re-attempt ${retryNumber}/${retryLimitClamped} after tool issues`);
840
1001
  recordStep(step);
1002
+ forcePrimaryRetry = modelRouting.applied;
841
1003
  turnRetries += 1;
842
1004
  const delay = Math.min(TURN_RETRY_BASE_DELAY_MS * 2 ** (turnRetries - 1), 2000);
843
1005
  await wait(delay);
@@ -875,6 +1037,7 @@ export const turnLoop = async (
875
1037
  turnRetries = 0;
876
1038
  continue;
877
1039
  }
1040
+ await recordJevShadow(step, msg, 'final_response');
878
1041
  step.events.push(finalResponse.finalMessage);
879
1042
  step.result = finalResponse.success ? '✅ Mission Success' : '❌ Mission Failure';
880
1043
  recordStep(step);
@@ -891,6 +1054,7 @@ export const turnLoop = async (
891
1054
  });
892
1055
  console.log(`[auto] → Injected DOM for next reasoning step`);
893
1056
  step.events.push(`[auto] → Injected DOM for next reasoning step`);
1057
+ await recordJevShadow(step, msg, 'after_observation');
894
1058
  // This is a meaningful turn even without tool calls — record it.
895
1059
  step.result = step.result || '🟡 In Progress';
896
1060
  recordStep(step);
@@ -0,0 +1,19 @@
1
+ # Cross-provider benchmark
2
+
3
+ This benchmark runs three example-repository missions across four fixed-model controls and four two-model OpenAI/Gemini portfolios. Jev selects one model at the mission boundary for portfolio conditions; that model owns the complete mission transcript.
4
+
5
+ The default design is 8 conditions × 5 randomized blocks × 3 missions, or 120 isolated mission executions. Failed missions are not retried with the other provider.
6
+
7
+ Preview the complete deterministic schedule without making API calls:
8
+
9
+ ```sh
10
+ npm run benchmark:cross-provider -- --dry-run
11
+ ```
12
+
13
+ Run or resume it:
14
+
15
+ ```sh
16
+ npm run benchmark:cross-provider
17
+ ```
18
+
19
+ Keys are loaded from `/home/shane/workspace/testronaut-examples/.env`. Progress, selections, logs, report paths, and a summary CSV are written under `experiments/results/cross-provider-pilot-v1/`. Re-running the command resumes completed work. Useful overrides include `--repetitions=5`, `--seed=...`, `--repo=...`, `--experiment-id=...`, and `--selection-threshold=0.55`.
@@ -0,0 +1,62 @@
1
+ # Optimization benchmark pilot
2
+
3
+ This benchmark compares seven optimization conditions across the OpenAI-focused
4
+ Testronaut Examples repository and the Gemini-focused Collegium repository. It
5
+ uses five randomized blocks by default: 35 runs per repository and 70 total.
6
+
7
+ The fixed mission subsets are:
8
+
9
+ - OpenAI: login, self-cleaning task creation/removal, and file transfer.
10
+ - Gemini: profile navigation, projects dashboard, and project workspace navigation.
11
+
12
+ Preview the complete deterministic schedule without making model calls:
13
+
14
+ ```bash
15
+ npm run benchmark:optimization -- --dry-run
16
+ ```
17
+
18
+ Run or resume both repositories:
19
+
20
+ ```bash
21
+ npm run benchmark:optimization
22
+ ```
23
+
24
+ Run only one target:
25
+
26
+ ```bash
27
+ npm run benchmark:optimization -- --target=openai
28
+ npm run benchmark:optimization -- --target=gemini
29
+ ```
30
+
31
+ Useful controls:
32
+
33
+ ```bash
34
+ npm run benchmark:optimization -- --repetitions=1 --experiment-id=optimization-smoke
35
+ npm run benchmark:optimization -- --seed=my-fixed-seed
36
+ ```
37
+
38
+ Progress is saved after every condition under target-specific
39
+ `experiments/results/<experiment-id>/state-openai.json` and
40
+ `state-gemini.json` files, so both loops may run concurrently without clobbering
41
+ state. Reusing the same experiment ID skips completed conditions. The matching
42
+ `runs-openai.csv` and `runs-gemini.csv` files contain one row per completed run,
43
+ and captured CLI output is stored under the corresponding `logs` directory.
44
+ Each generated Testronaut JSON report also records its experiment ID, condition,
45
+ and block. If state is lost, the runner recovers completed conditions from those
46
+ report fields before resuming.
47
+
48
+ The runner invokes this checkout's `bin/cli.js` directly and fixes the routing
49
+ probability at 0.40. It does not alter either target repository's configuration.
50
+ Model/API credentials must already be available in the environment or target
51
+ repository `.env` files.
52
+
53
+ After all 70 reports are present, generate the findings report and portable data
54
+ package with:
55
+
56
+ ```bash
57
+ npm run benchmark:package
58
+ ```
59
+
60
+ This writes a Markdown report, normalized CSV datasets, the raw JSON reports,
61
+ manifest, and checksums under `experiments/results/optimization-pilot-v1/`, plus
62
+ a compressed `optimization-pilot-v1-data-package.tar.gz` archive.