openmerit 0.1.6 → 0.1.7-preview.2

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 (60) hide show
  1. package/CHANGELOG.md +10 -4
  2. package/README.md +61 -40
  3. package/dist/eval/src/adapters.mjs +112 -0
  4. package/dist/eval/src/cli.mjs +151 -0
  5. package/dist/eval/src/config.mjs +108 -0
  6. package/dist/eval/src/process.mjs +44 -0
  7. package/dist/eval/src/report.mjs +59 -0
  8. package/dist/eval/src/runner.mjs +123 -0
  9. package/dist/eval/src/scout.mjs +37 -0
  10. package/dist/terminal/public/app.js +36 -36
  11. package/dist/terminal/public/flow.css +1 -1
  12. package/dist/terminal/public/flow.js +26 -26
  13. package/dist/terminal/public/index.html +3 -2
  14. package/dist/terminal/public/styles.css +320 -91
  15. package/dist/terminal/src/cli.mjs +2 -0
  16. package/dist/terminal/src/demo-policy.mjs +205 -0
  17. package/dist/terminal/src/enterprise-sample.mjs +151 -0
  18. package/docs/adapter-guide.md +2 -2
  19. package/docs/agent-evaluations.md +145 -0
  20. package/docs/architecture.md +3 -3
  21. package/docs/automation.md +5 -5
  22. package/docs/budgets.md +2 -2
  23. package/docs/commands.md +9 -5
  24. package/docs/demo-backfill.md +3 -1
  25. package/docs/demo-fieldkit.md +3 -1
  26. package/docs/demo-placement.md +3 -1
  27. package/docs/demo-spam.md +3 -1
  28. package/docs/demo-support.md +3 -1
  29. package/docs/demo.md +22 -5
  30. package/docs/first-trial.md +2 -2
  31. package/docs/getting-started.md +23 -12
  32. package/docs/index.md +18 -29
  33. package/docs/inference-terminal.md +90 -54
  34. package/docs/interactive-demo.md +118 -0
  35. package/docs/lifecycle.md +4 -4
  36. package/docs/metrics-and-evidence.md +3 -3
  37. package/docs/operations.md +3 -5
  38. package/docs/pi-extension.md +6 -6
  39. package/docs/roadmap.md +17 -17
  40. package/docs/scout.md +206 -0
  41. package/docs/security.md +8 -4
  42. package/docs/site-demo.md +4 -0
  43. package/docs/site-design.md +7 -1
  44. package/docs/site-documentation.md +9 -3
  45. package/docs/site-domain-migration.md +57 -0
  46. package/docs/site-dynamic-og.md +2 -2
  47. package/docs/site-faq-maintenance.md +133 -6
  48. package/docs/site-hero-resolution.md +2 -0
  49. package/docs/site-illustration-sequences.md +3 -1
  50. package/docs/site-illustration-transparency.md +80 -0
  51. package/docs/site-memo.md +6 -0
  52. package/docs/site-og-image.md +1 -1
  53. package/docs/site-publication-2026-10-14.md +29 -0
  54. package/docs/site-scout-decisions-experiment.md +87 -0
  55. package/docs/site-scout.md +254 -0
  56. package/docs/site-terminal-preview.md +96 -3
  57. package/docs/site-themes.md +30 -0
  58. package/docs/testing.md +46 -68
  59. package/docs/troubleshooting.md +3 -3
  60. package/package.json +9 -3
@@ -9,6 +9,7 @@ import { openBrowser } from './browser.mjs';
9
9
  const help = `OpenMerit · Inference Terminal
10
10
 
11
11
  openmerit dash [path] [options]
12
+ openmerit eval --help Evaluate CLI changes across coding agents
12
13
 
13
14
  --port <number> Require a specific local port (default: 4310 or a free port)
14
15
  --interval <seconds> Collection interval, 10–30 (default: 15)
@@ -26,6 +27,7 @@ Coding-harness usage is outside this terminal's scope.
26
27
 
27
28
  async function main() {
28
29
  const args = process.argv.slice(2);
30
+ if (args[0] === 'eval') { const { main } = await import('../../eval/src/cli.mjs'); await main(args.slice(1)); return; }
29
31
  if (!args.length || args.includes('--help') || args.includes('-h')) { console.log(help); return; }
30
32
  if (args.shift() !== 'dash') throw new Error('Unknown command. Run openmerit --help.');
31
33
  let root = process.cwd(), port = 4310, interval = 15, demo = false, json = false, pathSeen = false, discovery = true, noOpen = false, explicitPort = false, diagnostics = false;
@@ -0,0 +1,205 @@
1
+ // A reviewed model/service-offer catalog for the hosted fictional comparison only.
2
+ // Scores are publisher-reported point estimates, not insurance-workload evaluations.
3
+ export const EVIDENCE_DATE = '2026-10-10';
4
+ export const PREFERENCES = [
5
+ { value: 'default', label: 'Default · within your stack', note: 'Search all configured services; choose the lowest modeled cost among qualified replacements.' },
6
+ { value: 'zdr', label: 'Bias ZDR models', note: 'Prefer confirmed ZDR endpoints. Eligibility alone does not establish account approval or enabled settings.' },
7
+ { value: 'open', label: 'Bias open-weights models', note: 'Prefer models with open weights.' },
8
+ { value: 'closed', label: 'No open-weights models', note: 'Require a closed-weight assignment for every workload.' },
9
+ { value: 'usa', label: 'Bias USA models', note: 'Prefer models from US developers. This does not specify hosting location or data residency.' },
10
+ { value: 'china', label: 'Bias Chinese models', note: 'Prefer models from Chinese developers. This does not specify hosting location or data residency.' },
11
+ ];
12
+ export const preferenceFor = value => PREFERENCES.find(item => item.value === value) ?? PREFERENCES[0];
13
+ const oa = 'https://openai.com/index/gpt-4-1/';
14
+ const claude = 'https://www-cdn.anthropic.com/bbd8ef16d70b7a1665f14f306ee88b53f686aa75/Claude%20Sonnet%204.6%20System%20Card.pdf';
15
+ const gemini = 'https://storage.googleapis.com/deepmind-media/gemini/gemini_3_flash_model_evaluation.pdf';
16
+ const metric = (axis, name, value, suite, source, settings) => ({ axis, name, value, suite, source, settings, unit: '%', direction: 'higher' });
17
+ const oaScores = (tool, capability, reasoning) => [
18
+ metric('tools', 'τ-bench retail', tool, 'openai-4.1-tau-retail-gpt4o-user', oa, '5-run average; original GPT-4o user simulator; no custom tools or prompting.'),
19
+ metric('capabilities', 'MMLU', capability, 'openai-4.1-mmlu', oa, 'OpenAI GPT-4.1 release comparison; GPT-4o snapshot 2024-11-20.'),
20
+ metric('reasoning', 'AIME 2024', reasoning, 'openai-4.1-aime24', oa, 'OpenAI GPT-4.1 release comparison, AIME 2024.'),
21
+ ];
22
+ const claudeScores = (tool, capability, reasoning) => [
23
+ metric('tools', 'τ²-bench retail', tool, 'anthropic-sonnet46-tau2-retail', claude, 'Sonnet 4.6 system card §2.5; max effort, 10 trials for Sonnet; published Opus configuration.'),
24
+ metric('capabilities', 'MMMU-Pro · no tools', capability, 'anthropic-sonnet46-mmmupro-notools', claude, 'Sonnet 4.6 system card Table 2.1.A; publisher model configurations.'),
25
+ metric('reasoning', 'GPQA Diamond', reasoning, 'anthropic-sonnet46-gpqa', claude, 'Sonnet 4.6 system card §2.8; adaptive thinking, max effort for Sonnet; published Opus configuration.'),
26
+ ];
27
+ const googleScores = (tool, capability, reasoning) => [
28
+ metric('tools', 'τ²-bench', tool, 'google-3flash-tau2-fixed-airline', gemini, 'Gemini 3 Pro user simulator; fixed Airline domain; mean of Retail, Airline and Telecom.'),
29
+ metric('capabilities', 'MMMU-Pro', capability, 'google-3flash-mmmupro', gemini, 'Thinking; mean of Standard and Vision settings; no tools.'),
30
+ metric('reasoning', 'GPQA Diamond', reasoning, 'google-3flash-gpqa', gemini, 'Thinking; no tools; Google December 2025 comparison.'),
31
+ ];
32
+ const ossCard = 'https://arxiv.org/html/2508.10925v1';
33
+ const ossScores = (tool, capability, reasoning) => [
34
+ metric('tools', 'τ-bench retail', tool, 'openai-oss-high-retail', ossCard, 'Table 3; high reasoning effort; developer function calling.'),
35
+ metric('capabilities', 'MMLU', capability, 'openai-oss-high-mmlu', ossCard, 'Table 3; high reasoning effort, default system prompt, pass@1.'),
36
+ metric('reasoning', 'GPQA Diamond · no tools', reasoning, 'openai-oss-high-gpqa', ossCard, 'Table 3; high reasoning effort, no tools, pass@1.'),
37
+ ];
38
+ const dsCard = 'https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash';
39
+ const dsScores = (tool, capability, reasoning) => [
40
+ metric('tools', 'AutomationBench', tool, 'deepseek41-automation', dsCard, 'Frontier instruct-model comparison; official scaffold, maximum reasoning effort, temperature 1, top-p .95.'),
41
+ metric('capabilities', "Agent’s Last Exam", capability, 'deepseek41-ale', dsCard, 'Frontier instruct-model comparison; broad agent tasks, official scaffold, maximum reasoning effort.'),
42
+ metric('reasoning', 'GPQA Diamond', reasoning, 'deepseek41-gpqa', dsCard, 'Frontier instruct-model comparison; pass@1, maximum reasoning effort. Published model scores, not endpoint measurements.'),
43
+ ];
44
+ const qwenCard = 'https://huggingface.co/Qwen/Qwen3.6-27B';
45
+ const qwenScores = (tool, pass3, capability, reasoning) => [
46
+ metric('tools', 'ClawEval · average', tool, 'qwen36-claw-avg', qwenCard, 'Qwen 3.6 27B release comparison; published model configurations.'),
47
+ metric('tools', 'ClawEval · pass³', pass3, 'qwen36-claw-pass3', qwenCard, 'Same comparison, stricter repeated-task success. Both tool metrics must pass.'),
48
+ metric('capabilities', 'MMMU-Pro', capability, 'qwen36-mmmupro', qwenCard, 'Vision benchmark; published Qwen 3.6 comparison configurations.'),
49
+ metric('reasoning', 'GPQA Diamond', reasoning, 'qwen36-gpqa', qwenCard, 'Published Qwen 3.6 comparison configurations.'),
50
+ ];
51
+ const cohortScores = (source, suite, names, values, settings) => ['tools', 'capabilities', 'reasoning'].map((axis, index) => metric(axis, names[index], values[index], `${suite}-${axis}`, source, settings));
52
+ const flashCard = 'https://huggingface.co/zai-org/GLM-5.3-Flash';
53
+ const flashScores = values => cohortScores(flashCard, 'glm53flash-release', ['AutomationBench v1.0.6', 'GDPval-AA v2', 'HLE · with tools'], values, 'Published release chart and footnotes; maximum effort; GDPval-AA rating, not percent.').map(item => item.axis === 'capabilities' ? { ...item, unit: ' rating' } : item);
54
+ const qwen38Card = 'https://huggingface.co/Qwen/Qwen3.8-27B';
55
+ const qwen38Scores = values => cohortScores(qwen38Card, 'qwen38-release', ['ClawEval-MM · average', 'IFBench', 'GPQA Diamond'], values, 'Qwen 3.8 model card comparison; published thinking configurations. ClawEval-MM is distinct from ClawEval.');
56
+ // The application-code profile screens generated SQL/Python used by the
57
+ // insurance product. These are application workers, not developer-harness usage.
58
+ // Keep publisher cohorts separate: LiveCodeBench v6 % is not LCB Pro Elo.
59
+ const glm47Card = 'https://huggingface.co/zai-org/GLM-4.7';
60
+ const glmCodeScores = values => cohortScores(glm47Card, 'glm47-application-code', ['τ²-bench', 'LiveCodeBench v6', 'GPQA Diamond'], values, 'GLM-4.7 release table; thinking enabled, preserved thinking for multi-turn tools; adjusted user prompts and fixed Airline domain. Published comparator configurations; not endpoint parity.');
61
+ const googleCodeScores = values => cohortScores(gemini, 'google-3flash-application-code', ['τ²-bench', 'LiveCodeBench Pro', 'GPQA Diamond'], values, 'December 2025 comparison: Gemini thinking, Sonnet 4.5 high/best available thinking. LCB Pro official leaderboard Elo; τ² fixed Airline, Gemini user simulator for Gemini and publisher-reported Sonnet score. Public comparison, not a controlled endpoint evaluation.').map(item => item.axis === 'capabilities' ? { ...item, unit: ' Elo' } : item);
62
+ const basetenPolicy = { confirmedZdr: true, policySource: 'https://www.baseten.co/security-practices/', endpointMode: 'Synchronous Model API; request/response logging off' };
63
+ const model = (id, name, provider, input, output, source, benchmarks = [], extra = {}) => ({ id, modelId: id, name, provider, input, output, source, benchmarks, creator: provider, origin: 'US', openWeights: false, outputType: 'text', confirmedZdr: false, ...extra });
64
+ const openai = name => `https://developers.openai.com/api/docs/models/${name}`;
65
+ const anthropic = 'https://platform.claude.com/docs/en/about-claude/pricing';
66
+ const google = 'https://ai.google.dev/gemini-api/docs/pricing';
67
+ const groq = 'https://console.groq.com/docs/models';
68
+ const baseten = 'https://www.baseten.co/pricing/';
69
+ const mistral = 'https://docs.mistral.ai/inference/pricing';
70
+ export const CATALOG = [
71
+ model('gpt4o', 'GPT-4o', 'OpenAI', 2.5, 10, openai('gpt-4o'), oaScores(60.3, 85.7, 13.1)),
72
+ model('gpt41', 'GPT-4.1', 'OpenAI', 2, 8, openai('gpt-4.1'), oaScores(68, 90.2, 48.1)),
73
+ model('gpt41mini', 'GPT-4.1 mini', 'OpenAI', .4, 1.6, openai('gpt-4.1-mini'), oaScores(55.8, 87.5, 49.6)),
74
+ model('gpt54', 'GPT-5.4', 'OpenAI', 2.5, 15, openai('gpt-5.4')),
75
+ model('gpt54mini', 'GPT-5.4 mini', 'OpenAI', .75, 4.5, openai('gpt-5.4-mini')),
76
+ model('opus45', 'Claude Opus 4.5', 'Anthropic', 5, 25, anthropic, [...claudeScores(88.9, 70.6, 87), ...qwenScores(76.6, 59.6, 70.6, 87)]),
77
+ model('sonnet46', 'Claude Sonnet 4.6', 'Anthropic', 3, 15, anthropic, claudeScores(91.7, 74.5, 89.9)),
78
+ model('sonnet45', 'Claude Sonnet 4.5', 'Anthropic', 3, 15, anthropic, [...glmCodeScores([87.2, 64.0, 83.4]), ...googleCodeScores([87.2, 1418, 83.4])], { configuration: 'Thinking enabled; published comparison configurations' }),
79
+ model('haiku45', 'Claude Haiku 4.5', 'Anthropic', 1, 5, anthropic),
80
+ model('gemini25pro', 'Gemini 2.5 Pro', 'Google', 1.25, 10, google, googleScores(77.8, 68, 86.4)),
81
+ model('gemini25flash', 'Gemini 2.5 Flash', 'Google', .3, 2.5, google, googleScores(79.5, 66.7, 82.8)),
82
+ model('gemini3flash', 'Gemini 3 Flash Preview', 'Google', .5, 3, google, [...googleScores(90.2, 81.2, 90.4), ...googleCodeScores([90.2, 2316, 90.4])]),
83
+ model('oss120', 'gpt-oss-120b', 'Groq', .15, .6, groq, ossScores(67.8, 90, 80.1), { creator: 'OpenAI', openWeights: true, configuration: 'High reasoning effort' }),
84
+ model('oss20', 'gpt-oss-20b', 'Groq', .075, .3, groq, ossScores(54.8, 85.3, 71.5), { creator: 'OpenAI', openWeights: true, configuration: 'High reasoning effort' }),
85
+ model('glm53', 'GLM-5.3', 'Baseten', 1.4, 4.4, baseten, dsScores(48.8, 28.5, 88.1), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Maximum reasoning effort' }),
86
+ model('glm53flash', 'GLM-5.3-Flash', 'Baseten', .15, .5, baseten, flashScores([48.8, 1773, 55.3]), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy }),
87
+ model('mistral35', 'Mistral Medium 3.5', 'Mistral', 1.5, 7.5, mistral, [], { origin: 'FR', openWeights: true, metadataSource: 'https://docs.mistral.ai/models/mistral-medium-3-5-26-04' }),
88
+ model('mistral4', 'Mistral Small 4', 'Mistral', .15, .6, mistral, [], { origin: 'FR', openWeights: true }),
89
+ model('oss120baseten', 'gpt-oss-120b', 'Baseten', .1, .5, baseten, ossScores(67.8, 90, 80.1), { modelId: 'oss120', creator: 'OpenAI', openWeights: true, ...basetenPolicy, configuration: 'High reasoning effort; same published checkpoint' }),
90
+ model('deepseek41flash', 'DeepSeek V4.1 Flash', 'Baseten', .3, 1.2, baseten, dsScores(54.8, 31.8, 90.9), { creator: 'DeepSeek', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Maximum reasoning effort (100)' }),
91
+ model('deepseek4pro0813', 'DeepSeek V4 Pro 0813', 'Baseten', 1.32, 3.96, baseten, [], { creator: 'DeepSeek', origin: 'CN', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/deepseek-ai/DeepSeek-V4-Pro-0813' }),
92
+ model('nemotron3ultra', 'Nemotron 3 Ultra', 'Baseten', .6, 2.4, baseten, cohortScores('https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16', 'nvidia-ultra-nemo', ['TauBench V3 · average', 'MMLU-Pro', 'GPQA · no tools'], [70.9, 86.8, 87.0], 'NVIDIA NeMo evaluation SDK; model-card configurations. TauBench V3 is not τ-bench or τ²-bench.'), { creator: 'NVIDIA', origin: 'US', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B-BF16' }),
93
+ model('glm52', 'GLM-5.2', 'Baseten', 1.4, 4.4, baseten, flashScores([26.2, 1504, 54.7]), { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, metadataSource: 'https://huggingface.co/zai-org/GLM-5.2' }),
94
+ model('glm47', 'GLM-4.7', 'Baseten', .6, 2.2, 'https://www.baseten.co/library/glm-4-7/', [...cohortScores('https://huggingface.co/zai-org/GLM-4.7', 'glm47-release', ['τ²-bench', 'MMLU-Pro', 'GPQA Diamond'], [87.4, 84.3, 85.7], 'GLM-4.7 release configurations; preserved thinking, adjusted user prompts and fixed Airline domain for τ²-bench.'), ...glmCodeScores([87.4, 84.9, 85.7])], { creator: 'Z.ai', origin: 'CN', openWeights: true, ...basetenPolicy, configuration: 'Thinking enabled', metadataSource: 'https://huggingface.co/zai-org/GLM-4.7' }),
95
+ model('qwen36', 'Qwen 3.6 · 27B', 'Groq', .6, 3, groq, [...qwenScores(72.4, 60.6, 75.8, 87.8), ...qwen38Scores([50.4, 69.1, 87.8])], { creator: 'Alibaba', origin: 'CN', openWeights: true, availability: 'Preview', metadataSource: qwenCard }),
96
+ model('qwen38', 'Qwen 3.8 · 27B', 'Groq', .8, 4, groq, qwen38Scores([56.9, 79.5, 89.2]), { creator: 'Alibaba', origin: 'CN', openWeights: true, availability: 'Preview', metadataSource: 'https://huggingface.co/Qwen/Qwen3.8-27B' }),
97
+ model('jev', 'Jev 1.13', 'TypeSafe', .042, 0, 'https://docs.typesafe.ai/models', [], { outputType: 'decision' }),
98
+ ];
99
+ export const CONFIGURED_SERVICES = ['OpenAI', 'Anthropic', 'Google', 'Groq', 'Baseten', 'Mistral'];
100
+ const byId = id => CATALOG.find(item => item.id === id);
101
+ // Every workload is fictional application work. Names are plausible declarations,
102
+ // never represented as discoveries from a visitor's repository.
103
+ const APPLICATION_CODE_PROFILES = {
104
+ 'actuarial.queries': { input: 14000, output: 700 },
105
+ 'portfolio.reporting': { input: 18000, output: 900 },
106
+ 'claims.reconciliation': { input: 5000, output: 2000 },
107
+ 'reserves.analysis': { input: 6000, output: 2400 },
108
+ };
109
+ export const WORKLOADS = [
110
+ ['claims.intake', 'Claim intake', 'gpt4o', ['parse_notice', 'extract_loss_date', 'normalize_address', 'link_policy', 'check_attachments', 'summarize_notice', 'prepare_case']],
111
+ ['policy.extraction', 'Policy document extraction', 'gpt54', ['read_schedule', 'extract_limits', 'extract_deductibles', 'identify_endorsements', 'normalize_terms', 'link_sections', 'check_completeness']],
112
+ ['coverage.review', 'Coverage review support', 'opus45', ['retrieve_wording', 'compare_endorsements', 'locate_exclusions', 'cite_conditions', 'summarize_scope', 'flag_ambiguity', 'prepare_review']],
113
+ ['claims.correspondence', 'Claim correspondence', 'sonnet46', ['draft_acknowledgment', 'request_documents', 'summarize_progress', 'draft_follow_up', 'check_tone', 'cite_case_notes', 'prepare_handoff']],
114
+ ['documents.verification', 'Document verification', 'gemini25pro', ['read_estimate', 'extract_line_items', 'match_invoices', 'check_totals', 'compare_versions', 'flag_missing_pages', 'prepare_evidence']],
115
+ ['renewals.summary', 'Renewal preparation', 'gemini25flash', ['summarize_history', 'compare_schedules', 'list_changes', 'check_dates', 'extract_terms', 'prepare_brief', 'draft_reminder']],
116
+ ['claims.tagging', 'Claim tagging', 'oss120', ['tag_document', 'label_channel', 'extract_reference', 'normalize_category', 'link_attachment', 'label_language', 'index_summary']],
117
+ ['fraud.review', 'Fraud review support', 'glm53', ['summarize_signals', 'link_evidence', 'compare_statements', 'flag_inconsistency', 'build_timeline', 'prepare_case_note', 'route_to_reviewer']],
118
+ ['broker.translation', 'Broker correspondence', 'mistral35', ['detect_language', 'translate_email', 'preserve_policy_terms', 'check_names', 'summarize_request', 'draft_reply', 'prepare_review']],
119
+ ['claims.routing', 'Decision routing', 'gpt4o', ['route_notice', 'rank_queue', 'classify_document', 'score_completeness', 'flag_duplicates', 'choose_workflow', 'route_exception']],
120
+ ['actuarial.queries', 'Actuarial query generation', 'sonnet45', ['read_schema', 'select_tables', 'generate_query', 'check_join_keys', 'validate_query', 'explain_result', 'prepare_review']],
121
+ ['claims.reconciliation', 'Claim reconciliation scripts', 'sonnet45', ['read_line_items', 'normalize_columns', 'generate_transform', 'compare_totals', 'build_checks', 'explain_variance', 'prepare_review']],
122
+ ['portfolio.reporting', 'Portfolio reporting queries', 'sonnet45', ['read_report_spec', 'map_dimensions', 'generate_query', 'check_aggregates', 'validate_periods', 'explain_result', 'prepare_report']],
123
+ ['reserves.analysis', 'Reserve analysis scripts', 'sonnet45', ['read_triangle', 'normalize_periods', 'generate_calculation', 'check_assumptions', 'build_sensitivity', 'explain_projection', 'prepare_review']],
124
+ ].map(([id, purpose, before, tasks]) => ({
125
+ id, purpose, before: byId(before), configuredServices: CONFIGURED_SERVICES, tasks,
126
+ benchmarkProfile: APPLICATION_CODE_PROFILES[id] ? 'Application code · tools, code generation, reasoning' : 'General application · tools, capabilities, reasoning',
127
+ tokenProfile: APPLICATION_CODE_PROFILES[id] ?? null,
128
+ requiredAxes: ['tools', 'capabilities', 'reasoning', ...(id === 'claims.routing' ? ['decisions'] : [])],
129
+ }));
130
+
131
+ export function modeledCost(model, usage) {
132
+ return model && [model.input, model.output, usage.inputTokens, usage.outputTokens].every(value => Number.isFinite(value) && value >= 0)
133
+ ? (model.input * usage.inputTokens + model.output * usage.outputTokens) / 1e6 : null;
134
+ }
135
+ export function assessReplacement(workload, candidate, usage) {
136
+ const before = workload.before;
137
+ const checks = workload.requiredAxes.flatMap(axis => axis === 'decisions' ? ['decisionAccuracy', 'calibration'] : [axis]).flatMap(axis => {
138
+ const baselines = before.benchmarks.filter(item => item.axis === axis);
139
+ const pairs = baselines.flatMap(a => candidate.benchmarks.filter(b => b.axis === axis && a.suite === b.suite && a.name === b.name && a.unit === b.unit && a.direction === b.direction).map(b => ({ a, b })));
140
+ if (!pairs.length) return [{ axis, before: baselines[0] ?? null, after: candidate.benchmarks.find(item => item.axis === axis) ?? null, status: 'missing' }];
141
+ // Check every matched metric, not the first passing score in a cohort.
142
+ return pairs.map(({ a, b }) => {
143
+ const comparable = Boolean(a.source && b.source && Number.isFinite(a.value) && Number.isFinite(b.value));
144
+ const pass = comparable && (a.direction === 'lower' ? b.value <= a.value : b.value >= a.value);
145
+ return { axis, before: a, after: b, status: !comparable ? 'missing' : pass ? 'pass' : 'regression' };
146
+ });
147
+ });
148
+ const beforeCost = modeledCost(before, usage), afterCost = modeledCost(candidate, usage);
149
+ const cheaper = beforeCost !== null && afterCost !== null && afterCost < beforeCost;
150
+ // Only paired latency measurements with the same workload/configuration qualify.
151
+ // No latency data is fabricated or inferred from token generation speed in this demo.
152
+ const a = before.latency, b = candidate.latency;
153
+ const faster = Boolean(a && b && a.suite && a.metric && a.suite === b.suite && a.metric === b.metric && a.source && b.source && Number.isFinite(a.value) && Number.isFinite(b.value) && b.value > 0 && b.value < a.value);
154
+ const compatible = (workload.configuredServices ?? [before.provider]).includes(candidate.provider) && candidate.outputType === before.outputType && afterCost !== null;
155
+ const issues = [];
156
+ if (!(workload.configuredServices ?? [before.provider]).includes(candidate.provider)) issues.push('Service is not configured in this environment');
157
+ if (candidate.outputType !== before.outputType) issues.push('Different output contract; application adaptation required');
158
+ if (checks.some(check => check.status === 'missing')) issues.push('Comparable benchmark evidence is incomplete');
159
+ if (checks.some(check => check.status === 'regression')) issues.push('A required benchmark regresses');
160
+ if (afterCost === null) issues.push('Token price is unavailable');
161
+ if (!cheaper && !faster) issues.push('No verified cost or latency improvement');
162
+ return { candidate, checks, cheaper, faster, beforeCost, afterCost, compatible, issues, eligible: issues.length === 0 };
163
+ }
164
+ export function matchesPreference(model, preference) {
165
+ return preference === 'zdr' ? model.confirmedZdr === true : preference === 'open' ? model.openWeights === true : preference === 'closed' ? model.openWeights === false : preference === 'usa' ? model.origin === 'US' : preference === 'china' ? model.origin === 'CN' : true;
166
+ }
167
+ export function selectReplacement(workload, usage, preference = 'default') {
168
+ preference = preferenceFor(preference).value;
169
+ const assessments = (workload.candidates ?? CATALOG).filter(candidate => candidate.id !== workload.before.id).map(candidate => assessReplacement(workload, candidate, usage));
170
+ const allowed = item => item.eligible && (preference !== 'closed' || item.candidate.openWeights === false);
171
+ const byCost = (a, b) => (a.afterCost ?? Infinity) - (b.afterCost ?? Infinity) || a.candidate.id.localeCompare(b.candidate.id);
172
+ const qualified = assessments.filter(allowed).sort(byCost);
173
+ const defaultChoice = assessments.filter(item => item.eligible).sort(byCost)[0]?.candidate ?? workload.before;
174
+ // A preference first seeks a fully qualified match. Retaining an already
175
+ // matching baseline needs no exception. Only then relax benchmark/efficiency
176
+ // criteria, never the configured service, output contract or known pricing.
177
+ let selected = preference === 'default' ? qualified[0] : qualified.find(item => matchesPreference(item.candidate, preference));
178
+ if (!selected && preference !== 'default' && !matchesPreference(workload.before, preference)) {
179
+ const axes = (item, status) => new Set(item.checks.filter(check => check.status === status).map(check => check.axis)).size;
180
+ selected = assessments.filter(item => item.compatible && matchesPreference(item.candidate, preference)).sort((a, b) =>
181
+ axes(a, 'regression') - axes(b, 'regression') || axes(a, 'missing') - axes(b, 'missing') ||
182
+ Number(!a.cheaper && !a.faster) - Number(!b.cheaper && !b.faster) || byCost(a, b))[0];
183
+ if (!selected && preference !== 'closed') selected = qualified[0];
184
+ }
185
+ const proposed = selected?.candidate ?? workload.before;
186
+ const unresolved = preference === 'closed' && proposed.openWeights !== false;
187
+ const after = unresolved ? null : proposed;
188
+ const override = Boolean(after && selected && !selected.eligible);
189
+ const overrideReasons = override ? selected.issues : [];
190
+ const serviceChanged = Boolean(after && after.provider !== workload.before.provider);
191
+ const modelChanged = Boolean(after && after.modelId !== workload.before.modelId);
192
+ return {
193
+ id: workload.id, purpose: workload.purpose, benchmarkProfile: workload.benchmarkProfile, tokenProfile: workload.tokenProfile, before: workload.before, after,
194
+ changed: Boolean(after && selected), serviceChanged, modelChanged,
195
+ checks: selected?.checks ?? [], assessments, qualifiedCount: qualified.length,
196
+ preferenceMatched: Boolean(after && matchesPreference(after, preference)),
197
+ preferenceChanged: Boolean(after && after.id !== defaultChoice.id),
198
+ defaultChoice: defaultChoice.id, unresolved, override, overrideReasons,
199
+ reason: unresolved ? 'No compatible closed-weight assignment in the reviewed catalog. Service, output contract and known pricing remain required.'
200
+ : override ? `Selected for ${preferenceFor(preference).label}. No preferred replacement passed every primary check.`
201
+ : selected ? `${modelChanged ? 'Selected benchmark scores match or improve' : 'Same model checkpoint and published benchmark scores; service changes'}; ${selected.cheaper ? 'modeled token cost is lower' : 'paired latency is lower'}.`
202
+ : preference !== 'default' && matchesPreference(workload.before, preference) ? 'Baseline retained: already matches this preference; no qualifying preferred replacement.'
203
+ : 'Baseline retained. No reviewed candidate passes every required check.',
204
+ };
205
+ }
@@ -0,0 +1,151 @@
1
+ import { sampleSnapshot } from './sample.mjs';
2
+ import { WORKLOADS, CATALOG, CONFIGURED_SERVICES, PREFERENCES, preferenceFor, selectReplacement } from './demo-policy.mjs';
3
+
4
+ // Published standard, uncached text-token rates in USD per million tokens.
5
+ // This catalog belongs to the fictional hosted demonstration, never live billing.
6
+ export const PRICE_DATE = '2026-10-10';
7
+ const openai = model => `https://developers.openai.com/api/docs/models/${model}`;
8
+ const anthropic = 'https://platform.claude.com/docs/en/about-claude/pricing';
9
+ const google = 'https://ai.google.dev/gemini-api/docs/pricing';
10
+ const groq = 'https://console.groq.com/docs/models';
11
+ const baseten = 'https://www.baseten.co/pricing/';
12
+ const mistral = 'https://docs.mistral.ai/inference/pricing';
13
+ const rate = (name, provider, input, output, source, creator = provider) => ({ name, provider, input, output, source, creator });
14
+ export const PRICES = [
15
+ rate('GPT-5.4', 'OpenAI', 2.5, 15, openai('gpt-5.4')),
16
+ rate('GPT-5.4 mini', 'OpenAI', .75, 4.5, openai('gpt-5.4-mini')),
17
+ rate('GPT-5.4 nano', 'OpenAI', .2, 1.25, openai('gpt-5.4-nano')),
18
+ rate('Claude Opus 4.6', 'Anthropic', 5, 25, anthropic),
19
+ rate('Claude Sonnet 4.6', 'Anthropic', 3, 15, anthropic),
20
+ rate('Claude Haiku 4.5', 'Anthropic', 1, 5, anthropic),
21
+ rate('Gemini 2.5 Pro', 'Google', 1.25, 10, google),
22
+ rate('Gemini 2.5 Flash', 'Google', .3, 2.5, google),
23
+ rate('Gemini 2.5 Flash-Lite', 'Google', .1, .4, google),
24
+ rate('gpt-oss-120b', 'Groq', .15, .6, groq, 'OpenAI'),
25
+ rate('gpt-oss-20b', 'Groq', .075, .3, groq, 'OpenAI'),
26
+ rate('GLM-5.3', 'Baseten', 1.4, 4.4, baseten, 'Z.ai'),
27
+ rate('GLM-5.3-Flash', 'Baseten', .15, .5, baseten, 'Z.ai'),
28
+ rate('Mistral Medium 3.5', 'Mistral', 1.5, 7.5, mistral),
29
+ rate('Mistral Small 4', 'Mistral', .15, .6, mistral),
30
+ ];
31
+ export function tokenCost(price, input, output) {
32
+ if (!price || !Number.isFinite(input) || !Number.isFinite(output)) return null;
33
+ return (input * price.input + output * price.output) / 1_000_000;
34
+ }
35
+ const priceFor = (provider, model) => PRICES.find(price => price.provider === provider && price.name === model);
36
+ const REQUESTS_PER_GROUP = 1000;
37
+
38
+ // A bounded set of explicit synthetic groups, not millions of duplicated rows.
39
+ // Tokens and cost are group totals. The request count is sample-only metadata.
40
+ export function enterpriseSnapshot(now = Date.now()) {
41
+ const data = sampleSnapshot(now);
42
+ data.simulation = { kind: 'enterprise-groups', days: 30, requestsPerGroup: REQUESTS_PER_GROUP, priceDate: PRICE_DATE, prices: PRICES };
43
+ data.project = { name: 'enterprise-sample', scope: 'Fictional enterprise environment' };
44
+ const replacements = new Map([
45
+ ['GPT-5', priceFor('OpenAI', 'GPT-5.4')], ['GPT-5 mini', priceFor('OpenAI', 'GPT-5.4 mini')], ['GPT-5 nano', priceFor('OpenAI', 'GPT-5.4 nano')],
46
+ ['Claude Sonnet 4.5', priceFor('Anthropic', 'Claude Sonnet 4.6')], ['Claude Opus 4.1', priceFor('Anthropic', 'Claude Opus 4.6')], ['Claude Haiku 3.5', priceFor('Anthropic', 'Claude Haiku 4.5')],
47
+ ['DeepSeek R1', priceFor('Baseten', 'GLM-5.3')], ['Mistral Medium 3', priceFor('Mistral', 'Mistral Medium 3.5')],
48
+ ]);
49
+ const rename = (item, field) => { const price = replacements.get(item[field]); if (price) { item[field] = price.name; item.modelCreator = price.creator; } };
50
+ data.models.forEach(item => rename(item, 'name'));
51
+ data.deployments.forEach(item => { rename(item, 'model'); if (item.provider === 'Baseten') item.servingMode = 'shared'; });
52
+ data.records.forEach((record, index) => {
53
+ rename(record, 'model');
54
+ record.sampleRequests = REQUESTS_PER_GROUP;
55
+ record.occurredAt = new Date(now - (now - Date.parse(record.occurredAt)) * 30).toISOString();
56
+ const shard = index % 5 + 1;
57
+ record.agentId += `.worker-${shard}`;
58
+ if (record.agent) record.agent += `.worker-${shard}`;
59
+ record.taskId += `-partition-${shard}`;
60
+ record.inputTokens *= REQUESTS_PER_GROUP;
61
+ if (record.status === 'running') { record.status = 'completed'; record.outputTokens = 800; }
62
+ if (record.outputTokens != null) record.outputTokens *= REQUESTS_PER_GROUP;
63
+ record.cacheReadTokens = 0;
64
+ record.cost = tokenCost(priceFor(record.provider, record.model), record.inputTokens, record.outputTokens);
65
+ record.costKind = 'estimated';
66
+ record.requestId = null;
67
+ });
68
+ for (const source of data.sources) {
69
+ if (source.id === 'sample-activity') {
70
+ source.records = 9_600_000;
71
+ source.detail = 'Fictional request groups · 30 days';
72
+ source.note = '9.6 million fictional requests represented by 9,600 groups of 1,000. Tokens and spend are group totals. Published uncached text-token prices are applied where available; other costs are unknown. No application or provider was contacted.';
73
+ }
74
+ }
75
+ return data;
76
+ }
77
+
78
+ export function demoSnapshot(now = Date.now(), stage = 'before', preference = 'default') {
79
+ if (!['before', 'after'].includes(stage)) throw new Error('Unknown comparison stage');
80
+ preference = preferenceFor(preference).value;
81
+ const data = enterpriseSnapshot(now);
82
+ data.project = { name: 'insurance-tech-demo', scope: 'Fictional insurance operations application' };
83
+ const usage = WORKLOADS.map(() => ({ inputTokens: 0, outputTokens: 0 }));
84
+ data.records.forEach((record, index) => {
85
+ record.assignmentId = index % WORKLOADS.length;
86
+ const profile = WORKLOADS[record.assignmentId].tokenProfile;
87
+ if (profile) {
88
+ // Fixed fictional application profiles, independent of stage/preference.
89
+ // Schema-heavy SQL prompts and longer reconciliation scripts have distinct
90
+ // input/output mixes. Both include any billed reasoning output.
91
+ const variation = .85 + (index * 17 % 31) / 100;
92
+ record.inputTokens = Math.round(profile.input * variation) * REQUESTS_PER_GROUP;
93
+ record.outputTokens = Math.round(profile.output * variation) * REQUESTS_PER_GROUP;
94
+ }
95
+ if (!record.outputTokens) record.outputTokens = (80 + index * 37 % 1600) * REQUESTS_PER_GROUP;
96
+ usage[record.assignmentId].inputTokens += record.inputTokens;
97
+ usage[record.assignmentId].outputTokens += record.outputTokens;
98
+ });
99
+ const plans = Object.fromEntries(PREFERENCES.map(({ value }) => [value, WORKLOADS.map((workload, index) => selectReplacement(workload, usage[index], value))]));
100
+ const assignments = plans[preference];
101
+ const signature = pairs => JSON.stringify(pairs.map(pair => pair.after?.id ?? null));
102
+ const equivalentPreferences = Object.keys(plans).filter(value => value !== preference && signature(plans[value]) === signature(assignments));
103
+ const feasible = assignments.every(pair => !pair.unresolved);
104
+ data.simulation = { ...data.simulation, kind: 'comparison-groups', stage, preference, feasible, equivalentPreferences, catalogSize: CATALOG.filter(model => CONFIGURED_SERVICES.includes(model.provider)).length, configuredServices: CONFIGURED_SERVICES, assignments, prices: [...new Map(assignments.flatMap(pair => [pair.before, pair.after]).filter(Boolean).map(model => [model.id, model])).values()] };
105
+ const models = new Map(), deployments = new Map();
106
+ data.records.forEach((record, index) => {
107
+ const assignmentId = record.assignmentId, assignment = assignments[assignmentId], workload = WORKLOADS[assignmentId];
108
+ const price = stage === 'after' ? assignment.after ?? assignment.before : assignment.before, sequence = Math.floor(index / WORKLOADS.length);
109
+ const worker = String(sequence % 20 + 1).padStart(2, '0');
110
+ record.workload = `services/${workload.id.replaceAll('.', '/')}`;
111
+ record.agentId = record.agent = `${workload.id}.worker-${worker}`;
112
+ record.task = workload.tasks[sequence % workload.tasks.length];
113
+ record.taskId = `${workload.id}.${record.task}.batch-${Math.floor(sequence / workload.tasks.length) % 10 + 1}`;
114
+ record.model = price.name; record.modelCreator = price.creator; record.provider = price.provider;
115
+ record.location = 'cloud'; record.status = 'completed'; record.durationMs = null;
116
+ record.cost = tokenCost(price, record.inputTokens, record.outputTokens);
117
+ record.beforeCost = tokenCost(assignment.before, record.inputTokens, record.outputTokens);
118
+ record.afterCost = tokenCost(assignment.after, record.inputTokens, record.outputTokens);
119
+ record.routeKind = ['direct', 'router', 'gateway'][index % 3];
120
+ record.route = record.routeKind === 'direct' ? `${price.provider} API` : record.routeKind === 'router' ? 'insurance.router' : 'insurance-gateway';
121
+ record.path = [{ name: record.route, kind: record.routeKind }];
122
+ if (record.routeKind !== 'direct') record.path.push({ name: `${price.provider} API`, kind: 'direct' });
123
+ record.deploymentId = `demo-deployment-${assignmentId}`;
124
+ if (stage === 'after' && !assignment.after) return;
125
+ models.set(price.id, { id: `demo-model-${price.id}`, name: price.name, provider: price.provider, modelCreator: price.creator, sourceId: 'sample-activity', location: 'cloud', state: 'observed' });
126
+ deployments.set(assignmentId, { id: record.deploymentId, name: workload.id, model: price.name, modelCreator: price.creator, provider: price.provider, location: 'cloud', servingMode: 'shared', state: 'configured', sourceId: 'inventory', poolIds: [`pool-${price.provider.toLowerCase()}`] });
127
+ });
128
+ data.models = [...models.values()]; data.deployments = [...deployments.values()];
129
+ const providers = CONFIGURED_SERVICES;
130
+ data.inventory = [...providers.map(name => ({ name, kind: 'provider', location: 'cloud', state: 'configured', sourceId: 'inventory' })), { name: 'insurance.router', kind: 'router', location: 'cloud', state: 'configured' }, { name: 'insurance-gateway', kind: 'gateway', location: 'cloud', state: 'configured' }];
131
+ data.capacityPools = data.capacityPools.filter(pool => providers.some(name => pool.id === `pool-${name.toLowerCase()}`));
132
+ data.hosts = []; data.runtimeStats = []; delete data.machine;
133
+ data.sources = data.sources.filter(source => ['sample-activity', 'sample-capacity', 'inventory'].includes(source.id));
134
+ data.sources.find(source => source.id === 'sample-capacity').note = 'Fictional provider quota readings, held identical in both stages. They do not establish a change in latency, throughput or available capacity.';
135
+ data.sources.find(source => source.id === 'sample-activity').note += ' Insurance workflow and worker names are fictional declarations. Assignments distinguish qualified replacements, retained baselines and explicit preference overrides. Overrides do not pass all primary criteria. Application quality and latency have not been measured.';
136
+ data.simulation.windows = Object.fromEntries([0, 1, 6, 24, 168, 720].map(hours => {
137
+ const rows = data.records.filter(record => !hours || Date.parse(record.occurredAt) >= now - hours * 3600000);
138
+ const totals = selected => ({ requests: selected.length * REQUESTS_PER_GROUP, assignedRequests: selected.filter(record => assignments[record.assignmentId].after).length * REQUESTS_PER_GROUP, qualifiedRequests: selected.filter(record => assignments[record.assignmentId].after && !assignments[record.assignmentId].override).length * REQUESTS_PER_GROUP, overrideRequests: selected.filter(record => assignments[record.assignmentId].override).length * REQUESTS_PER_GROUP, beforeCost: selected.reduce((n, r) => n + r.beforeCost, 0), afterCost: feasible ? selected.reduce((n, r) => n + r.afterCost, 0) : null });
139
+ return [hours, { ...totals(rows), workloads: assignments.map((pair, index) => {
140
+ const selected = rows.filter(record => record.assignmentId === index);
141
+ return { ...totals(selected), afterCost: pair.after ? selected.reduce((n, r) => n + tokenCost(pair.after, r.inputTokens, r.outputTokens), 0) : null };
142
+ }) }];
143
+ }));
144
+ if (stage === 'after' && !feasible) {
145
+ data.records = data.records.filter(record => assignments[record.assignmentId].after);
146
+ const source = data.sources.find(source => source.id === 'sample-activity');
147
+ source.records = data.records.length * REQUESTS_PER_GROUP;
148
+ source.note += ' Incomplete After plan: only workloads with an assignment are included; preference overrides are identified in comparison metadata. Unresolved workloads remain in comparison metadata, without an After assignment or cost.';
149
+ }
150
+ return data;
151
+ }
@@ -67,7 +67,7 @@ const coordinator = new OpenMeritCoordinator(
67
67
 
68
68
  The confirmed task profile identifies the application LLM call under evaluation. Every metric, assessment, candidate, frontier artifact, and automation signal must carry that target ID; the harness's own model usage is orchestration data and must not be reported as product evidence. Application-provider cost, token, and latency telemetry belongs on the application signal or metric record.
69
69
 
70
- In the unreleased source checkout, setup may return `preliminaryModelLeads`: model IDs with sourced price estimates and reputation summaries. The field is optional under protocol 0.1 for older adapters. Core stores the leads and includes both the confirmed task profile and these leads in later discovery intent constraints, so a headless harness run does not depend on prior chat context. Leads are research priors, never measured candidate assessments.
70
+ Setup may return `preliminaryModelLeads`: model IDs with sourced price estimates and reputation summaries. The field is optional under protocol 0.1 for older adapters. Core stores the leads and includes both the confirmed task profile and these leads in later discovery intent constraints, so a headless harness run does not depend on prior chat context. Leads are research priors, never measured candidate assessments.
71
71
 
72
72
  ## External telemetry export
73
73
 
@@ -143,7 +143,7 @@ await coordinator.recordTaskObservation({
143
143
  modelId: "vendor/model",
144
144
  });
145
145
 
146
- // In the unreleased source checkout, core has already checked baseline
146
+ // Core has already checked baseline
147
147
  // sufficiency when the confirmed cadence became due. Inspect durable state
148
148
  // and dispatch only a pending downstream intent, if any.
149
149
  ```
@@ -0,0 +1,145 @@
1
+ # Evaluate CLI changes across coding agents
2
+
3
+ **npm preview `0.1.7-preview.2`.** Install from the `preview` tag; stable `openmerit@0.1.6` does not include these commands. This evaluates the coding agent with your CLI and documentation. The [Pi workflow](getting-started.md) still evaluates application inference; the [Inference Terminal](inference-terminal.md) remains read-only.
4
+
5
+ Compared with `0.1.7-preview.1`, this version adds [corrected cost accounting and blocked-execution handling](#accounting-and-blocked-executions), plus [budget selection after model scope](#budget-selection). It does not claim reconciled provider billing or a guaranteed exact model order.
6
+
7
+ One local runner supports Claude Code, Codex and OpenCode. Scout supplies candidates; local task checks measure each model/agent/CLI combination. Installing npm alone does not install or authenticate agents, modify project instructions, or start paid evaluations.
8
+
9
+ ## Set up
10
+
11
+ Install the preview and initialize it inside the project you want to evaluate:
12
+
13
+ ```sh
14
+ npm install -g openmerit@preview
15
+ openmerit eval init
16
+ openmerit eval doctor
17
+ ```
18
+
19
+ Init creates `.openmerit/eval.json`, a tiny extraction fixture, a tool and a standalone grader. It appends marked instructions to `AGENTS.md` and `CLAUDE.md`, preserving existing contents. Codex/OpenCode can read `AGENTS.md`; Claude Code reads `CLAUDE.md`. The instructions ask the agent to configure representative tasks, choose scope, obtain the user's budget, and rerun after CLI changes. These are instructions the host must follow, not guaranteed hooks. There are no install-time hooks or background services. A second init refuses to overwrite the suite.
20
+
21
+ Replace the starter before drawing product conclusions. Have your agent author representative fixtures and graders with you, and reserve fresh cases for final validation.
22
+
23
+ ## Choose scope and connections
24
+
25
+ ```sh
26
+ openmerit eval plan
27
+ ```
28
+
29
+ The first interactive plan asks: **existing project connections or all models Scout indexes?** It then asks for a **$10, custom or no-fixed-cap budget**. Both choices are saved. Noninteractive agents must ask the user and pass `--scope existing` or `--scope all` together with `--budget <USD|unlimited>` on first setup; missing input never chooses scope or spending automatically.
30
+
31
+ Add explicit routes to `connections` in the config, for example:
32
+
33
+ ```json
34
+ {
35
+ "id": "claude-project",
36
+ "agent": "claude",
37
+ "scoutId": "anthropic/claude-haiku-4.5",
38
+ "model": "claude-haiku-4-5-20251001",
39
+ "env": ["ANTHROPIC_API_KEY"]
40
+ }
41
+ ```
42
+
43
+ `scoutId` is Scout's catalogue ID. `model` is the native identifier your authenticated agent accepts. `command` optionally selects another executable path. `env` lists environment variable **names**, never values. Only basic OS variables and explicitly listed names pass to child agents. Saved authentication remains available through the normal home directory. No dotenv files are loaded automatically. Configure endpoint details through the native agent or named environment variables.
44
+
45
+ An SDK dependency or key-variable name does not prove access. The agent should inspect the project's intended connections and declare the models they can serve. This preview does not automatically enumerate endpoint catalogues. Multiple connections can test one Scout model through different agents/routes. Prefer pinned model versions; aliases can change.
46
+
47
+ Existing scope sends IDs and requires Scout to acknowledge filtering **before ranking five**. Fewer than five eligible models remain fewer. All scope searches the index, but missing authenticated routes block execution until configured and replanned. Inaccessible candidates are not silently replaced. Unranked Hugging Face specialists are not executed automatically.
48
+
49
+ Scout receives only each task's `description`, plus IDs for existing scope. Scout sends descriptions to OpenAI Decisions. Fixtures, execution prompts, endpoint URLs and credentials stay out of this request. The shortlist, warnings and ranking context are frozen locally. Scout scores are screening evidence, not measured success with your harness.
50
+
51
+ ## Tasks and variants
52
+
53
+ Each task needs `id`, `description` (for Scout, at most 4,000 characters), `prompt` (for execution), `fixture` (a project-relative directory) and `grader` (a standalone Node `.mjs` file).
54
+
55
+ Each variant needs `id`, `files` and `instructions`. The starter compares `bare` against `cli`. Bare omits your CLI/docs; native agent tools remain available. Across agents you are comparing different harnesses, with different permission systems. A no-native-tools experiment needs a separately controlled configuration.
56
+
57
+ Selected files, including uncommitted changes, are snapshotted before execution. Variant paths must not overwrite fixture inputs and retain their project-relative locations; fixture contents go at the workspace root. Include self-contained CLI artifacts and lean docs explicitly. Dependencies are not installed automatically; tools must be bundled or have their runtime requirements already available. Symlinks, parent-directory paths, `.env*`, `.git`, `.openmerit` and `node_modules` are rejected. Each selected snapshot is limited to 25 MB. Do not select credentials stored under other filenames.
58
+
59
+ The grader reads `{ "workspace": "/temporary/path", "output": "final response" }` from stdin and prints exactly `{ "quality": 0.0 }`, with a finite score from 0 to 1. It can check generated files using executable assertions. Keep it self-contained: imports beside it are not copied automatically. Graders are copied outside agent workspaces; never also include them or answer keys in fixtures/variant files. Test graders against correct and incorrect answers. A broken grader stops the experiment with unknown quality. Ordinary agent failures/timeouts count as quality zero; recognized budget, environment and connection blocks instead stop with unknown quality. See [failure classification](#accounting-and-blocked-executions).
60
+
61
+ ## Run after changes
62
+
63
+ The starter's `budget.maxUsd` is zero. After the user's scope and budget choices, for example:
64
+
65
+ ```sh
66
+ openmerit eval plan --scope existing --budget 10
67
+ openmerit eval run
68
+ # Edit your CLI or its documentation.
69
+ openmerit eval run
70
+ ```
71
+
72
+ For repeated local development:
73
+
74
+ ```sh
75
+ openmerit eval watch
76
+ ```
77
+
78
+ Watch runs initially, then after selected source/docs/fixture/grader/config bytes change and remain stable for two polls. Result writes do not trigger it. Runs never overlap; edits during execution become the next revision. Ctrl+C stops the watcher and active child process. No OS schedule or daemon is installed. `OPENMERIT_EVAL=1` tells cooperating agent instructions not to recurse into another evaluation.
79
+
80
+ Every attempt uses a new workspace and agent session. Each task runs balanced rounds across all selected connections and variants; order rotates and reverses. The default is 3–10 repetitions. After three, rounds continue while any group's quality range exceeds 5 percentage points, an attempt failed, or cost, input-token or latency variation has a coefficient of variation above 0.25. Ten is the maximum. Missing metrics cannot establish stability. Set `repeats.min` equal to `repeats.max` for fixed repetitions, useful for holdouts. This heuristic is not a significance test, and repetition is not task diversity.
81
+
82
+ The frozen plan survives CLI changes. Changes to descriptions, scope or connections require replanning. Previous-revision comparisons require matching prompts, fixtures, graders, routes, repeat settings, selected candidates, variant IDs, timeouts, agent versions, runner versions, invocation settings and reported model identities. CLI/docs bytes and variant instructions are the interventions. Keep external services and native agent settings stable too; they are not hermetically captured.
83
+
84
+ ## Metrics
85
+
86
+ ```sh
87
+ openmerit eval report
88
+ openmerit eval report --json
89
+ ```
90
+
91
+ Reports show quality mean/range, failures, mean input/output tokens, mean reported dollars and median full-process seconds per task/connection/variant. They retain Scout ranks but do not force a weighted winner. Lower cost with lower quality is a tradeoff. Matching complete revisions show deltas; partial/invalid experiments do not.
92
+
93
+ Claude provides aggregate and per-model usage and cost estimates, reconciled without counting the same usage twice. Codex JSON events provide tokens but not billed dollars, so cost stays unknown. OpenCode per-step usage/cost is summed across the episode, including tools; missing or zero step pricing leaves full cost unknown and preserves partial estimates. Input totals include separately reported cache reads/writes; cached tokens are retained separately without counting them twice. OpenCode output totals include separately reported reasoning tokens. Agent accounting and subscriptions differ. Reported costs are not reconciled provider bills; zero pricing is treated as unknown. Catalogue prices never replace unknown measured costs.
94
+
95
+ Preflight requires supported noninteractive JSON interfaces: Claude print JSON, Codex `exec --json --ephemeral --sandbox workspace-write`, and OpenCode `run --format json --auto`. OpenCode's auto option approves tool requests that are not explicitly denied, avoiding an interactive approval stall. Claude uses an explicit local tool allowlist without inherited MCP configuration; Codex and OpenCode retain native global settings. Recognized permission failures stop as connection problems, not model-quality evidence. OpenMerit does not add a sandbox around Claude/OpenCode. The old 2025 Codex CLI is unsupported. Native references: [Claude Code](https://code.claude.com/docs/en/headless), [Codex](https://developers.openai.com/codex/noninteractive), [OpenCode](https://opencode.ai/docs/cli/).
96
+
97
+ ## Budget and data
98
+
99
+ `budget.maxUsd` applies cumulatively across runs and restarts. Before each attempt the runner persists a reservation. It retains the greater of that reservation and the available full or partial cost estimate; estimates never release reserved allowance. Partial events never count as a complete bill. Insufficient remaining allowance stops a partial run. Raising maxUsd extends the allowance, not resets it. Do not delete unknown spend from the ledger. This is admission control, not a billing cutoff: one attempt may exceed its reservation. Finite budgets also use Claude's native maximum-budget flag. Use provider-side limits for hard monetary caps. Each attempt also has a timeout and 4 MB process-output limit. See [budget selection](#budget-selection) for the three choices and automatic reservations.
100
+
101
+ `.openmerit/eval/` holds `plan.json`, `budget.json`, `latest.json`, `runs/*.json` and `lock/pid`. Init ignores it in Git. Reports contain identities, metrics, hashes and status, not full transcripts or final outputs. Temporary files are removed afterward. Native agents/providers may retain their own data; Claude/Codex receive ephemeral flags, while OpenCode uses native storage. `.openmerit/eval.json` can be versioned without credentials.
102
+
103
+ Fresh workspaces are not security sandboxes. Trusted agents and graders execute with native permissions and user-level resources. Hard kills may leave temporary directories. After a crash, inspect the lock's PID and remove it only if that process is gone. This preview does not change Pi approvals, swap production models or upload results into Scout rankings.
104
+
105
+
106
+ ## VM acceptance limits
107
+
108
+ The published `0.1.7-preview.1` package was tested on an Ubuntu ARM64 VM with Node 22.23.1 on 15 October 2026. Claude Code 2.1.300 and OpenCode 1.19.0 executed a small CSV CLI task before and after a parser change. A separate OpenCode watcher completed three attempts per revision and emitted comparison deltas. Codex 0.162.1 passed interface preflight but could not execute filesystem tools because that VM denied the required Linux user namespaces. CLI flag checks do not establish that the native sandbox works on a host.
109
+
110
+ In that earlier `0.1.7-preview.1` test, a native budget stop could appear as a generic agent failure, and a successful final response explaining a host permission problem could receive zero from the task grader. These did not establish poor model quality; the corrections below address recognized native failures. The combined VM run also stopped before its final repetition because conservative reservations and agent cost estimates exhausted its local allowance; partial reports correctly omitted automatic comparison. Agent-reported cost differed substantially from OpenRouter account usage. This was one integration fixture, not validation of general task quality, exact model order, or billing accuracy.
111
+
112
+ ## Accounting and blocked executions
113
+
114
+ Since `0.1.7-preview.2`, recognized budget, sandbox, permission, authentication and model-access blocks are separate from task failures. A blocked attempt has `quality: null`, stops the run and cannot produce a revision comparison. Reports distinguish scored attempts, blocked attempts and task-check failures. Ordinary wrong outputs still receive their grader score; ordinary agent failures and configured timeouts still count as failures. Cancellation, a missing executable and output-limit interruption are unscored. Error classification uses native failure fields and failed-tool diagnostics, not arbitrary task text or model commentary. Unrecognized host failures can still require manual review.
115
+
116
+ On Linux, Codex preflight also runs a local sandbox probe before any model request or budget reservation. It uses the configured executable and the workspace-write policy; it does not weaken sandbox permissions. A failed probe produces `environment_blocked` with no task score. Doctor performs this same probe. Other operating systems still have interface checks only.
117
+
118
+ Claude's per-model totals recover usage omitted from aggregate budget-stop responses, without adding the same aggregate and model counters twice. Costs carry `costBasis` and remain native estimates. Zero or missing pricing is unknown; incomplete OpenCode step pricing stays partial. Partial costs survive into the row, summary and ledger, including failed attempts. No provider bill is reconstructed from these estimates, and no account-wide spend delta is assigned to individual attempts.
119
+
120
+ Budget admission now retains the greater of each reservation and its available full or partial estimate, including old ledger entries. Estimates never release a reservation. This may exhaust an existing allowance earlier; increasing it requires an explicit budget decision. The displayed reserved allowance is not billed spend, and a provider-side limit is still needed for a hard cap. Report version 2 prevents comparisons with the earlier accounting rules. There is no new provider integration, proxy, background service or model-specific rule.
121
+
122
+ ## Budget selection
123
+
124
+ After choosing existing connections or all indexed models, the first interactive `eval plan` asks:
125
+
126
+ 1. **$10** — a cumulative evaluation allowance for this project.
127
+ 2. **Custom amount** — asks how much in USD.
128
+ 3. **No fixed cap** — run the configured suite without a monetary admission limit.
129
+
130
+ Nothing is preselected. Blank, invalid or cancelled input cannot enable spending. Init still starts at zero. Newly generated project instructions tell coding agents to ask the same follow-up and pass the answer explicitly, for example:
131
+
132
+ ```sh
133
+ openmerit eval plan --scope existing --budget 10
134
+ # Alternatives after the user's choice:
135
+ openmerit eval plan --budget 25.50
136
+ openmerit eval plan --budget unlimited
137
+ ```
138
+
139
+ `--budget` belongs to `plan`, accepts positive USD amounts with at most two decimal places or the exact word `unlimited`, and never starts evaluation by itself. Noninteractive planning requires explicit flags when scope or budget has not been chosen. Existing positive budgets are reused. The choices are saved even if Scout subsequently fails, so a retry does not ask again. Init does not rewrite instructions in already-initialized projects; an agent in such a project can use these flags directly.
140
+
141
+ The allowance is cumulative across runs, watched edits, restarts and replanning. Changing from $10 to $20 raises the total allowance to $20; it does not add $20 or erase earlier reservations. Changing to or from no fixed cap also preserves the ledger. A normal `eval plan` reuses the saved choice. To change it, pass a new `--budget` value; setting `budget.maxUsd` back to zero in the config disables paid execution and prompts again on the next interactive plan.
142
+
143
+ Reservations are internal. New suites and budget choices use `reservationUsd: "auto"`: the smaller of $0.25 and the total allowance divided across the minimum planned attempts (tasks × selected connections × variants × minimum repetitions). No fixed cap uses $0.25 for ledger bookkeeping. This is a conservative starting allowance, not a prediction of an attempt's price or a guarantee the suite will finish. Higher reported charges still consume more allowance. Existing explicitly configured numeric reservations remain supported. Finite budgets retain Claude's native per-attempt budget flag; as before, an attempt can exceed its reservation, and provider limits are needed for a hard billing cap.
144
+
145
+ No fixed cap is stored explicitly as `budget.maxUsd: "unlimited"`; missing, null or invalid values never mean unlimited. It also removes Claude's per-attempt dollar limit, while keeping the same task count, 3–10 repetition bounds, timeouts and native permissions. Provider/account limits still apply. Usage and cost estimates remain recorded. `eval watch` can incur further charges after each selected change until stopped; Ctrl+C stops it and the active attempt. No background service or automatic production model change is added.
@@ -38,7 +38,7 @@ Contains deterministic behavior: choosing the next lifecycle intent according to
38
38
 
39
39
  `OpenMeritCoordinator` is the reusable adapter boundary. It constructs canonical intents, negotiates capabilities, records lifecycle progress and observations, validates results, and advances durable state. `OpenMeritStore` makes persistence replaceable; `ProjectStore` is the bundled filesystem backend.
40
40
 
41
- The coordinator also accepts idempotent application-target automation signals and evaluates the user-confirmed `AutomationCheckPolicy`. `buildAutomationPlan` tells an adapter which signals and wakeups are required and reports unsupported persistent scheduling honestly. OpenMerit never installs a scheduler or creates a paid cloud worker itself.
41
+ The coordinator also accepts idempotent application-target automation signals and evaluates the user-confirmed `AutomationCheckPolicy`. `buildAutomationPlan` tells an adapter which signals and wakeups are required and reports unsupported persistent scheduling honestly. Provisioning belongs to the adapter: Pi can install an authorized macOS user LaunchAgent; the coordinator itself does not create operating-system schedulers or paid cloud workers.
42
42
 
43
43
  ### `openmerit/pi`
44
44
 
@@ -46,9 +46,9 @@ Exposes the command surface, supplies private typed intent context, triggers Pi,
46
46
 
47
47
  The Pi package implements `HarnessAdapter`. It does not define canonical intents, per-intent result contracts, stage transitions, or validation rules.
48
48
 
49
- ### Inference Terminal (0.1.6)
49
+ ### Inference Terminal
50
50
 
51
- `packages/terminal` supplies a standalone `openmerit dash` executable, passive collectors, a loopback read-only HTTP API, and bundled dark-only browser assets. It does not invoke the coordinator or harness. One collector cycle reads existing metadata and runtime inventory; browser clients receive the same cached snapshot. Imported activity, runtime counters, and sampled whole-machine resources retain their separate scopes. Missing measurements remain unknown, and fictional sample data is isolated from the real snapshot. See the [terminal guide](inference-terminal.md) for the supported sources and JSON contract.
51
+ `packages/terminal` supplies a standalone `openmerit dash` executable, passive collectors, a loopback read-only HTTP API, and bundled browser assets. Stable npm 0.1.6 includes the terminal. The source interface and hosted dashboard follow the device’s light/dark preference; that appearance change is not included in npm 0.1.6. It does not invoke the coordinator or harness. One collector cycle reads existing metadata and runtime inventory; browser clients receive the same cached snapshot. Imported activity, runtime counters, and sampled whole-machine resources retain their separate scopes. Missing measurements remain unknown, and fictional sample data is isolated from the real snapshot. See the [terminal guide](inference-terminal.md) for the supported sources and JSON contract.
52
52
 
53
53
  ## Intelligence resources
54
54