openmerit 0.3.4 → 0.3.5

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.5 — 2026-11-05
4
+
5
+ - Add optional headless Langfuse setup through the CLI, HTTP API and `scout_connection` MCP tool. A separately approved `scout:connections` key manages the authenticated user's connection and evaluation data; recommendation keys retain their existing permission. Viewer credentials enter through protected CLI input or trusted client configuration, never model-visible tool arguments. The web connection UI remains available, with revised spacing, mobile controls and the official Langfuse logo. Hosted features require a matching Scout deployment; saving Langfuse credentials also requires its dedicated encryption secret. Publishing this package does not deploy or enable the hosted connector.
6
+
3
7
  ## 0.3.4 — 2026-11-05
4
8
 
5
9
  - `openmerit scout --results` refuses a file with more than 50 results before sending it, instead of failing with HTTP 413; split larger reports into files of up to 50.
@@ -141,6 +141,36 @@ aggregates only, never inputs, outputs or records.
141
141
  Scout ranks them above public benchmarks for this user only; models whose
142
142
  \`quality.measured\` is true rest on them.
143
143
 
144
+ Swap one agent at a time, holding the other agents, prompts, fixtures and controls
145
+ fixed. Use each agent's own checks to attribute failures. If all candidates score
146
+ 100%, the eval cannot separate their quality: add harder labelled cases before
147
+ claiming equivalent quality. Cost may break the observed tie, not prove equality.
148
+
149
+ If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
150
+ optional. Headless setup requires a separate connection-manager approval with
151
+ \`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
152
+ expiry; after approval, rerun that command. Existing recommendation keys cannot
153
+ manage connections. The user creates a separate Viewer key; receive it through
154
+ protected stdin or a secret-manager-injected environment, never chat, tool
155
+ arguments, shell literals, traced commands or tracked files. The CLI accepts
156
+ non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
157
+ and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
158
+ A separate trusted MCP connection with the management key exposes
159
+ \`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
160
+ must come from client-controlled HTTP headers outside model context; if the
161
+ client cannot do this securely, use protected CLI input or the web UI instead.
162
+ After they confirm connection and score/model mappings, call \`scout_models\` with
163
+ \`sync: "langfuse"\` to refresh aggregate measurements before recommending. The CLI
164
+ equivalent is \`openmerit scout --conversation <id> --sync langfuse\`. Never read
165
+ trace bodies to work around missing mappings or untagged runs; use the aggregate
166
+ report path instead. Results are stored per user, used in new conversations and
167
+ expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
168
+ Delete all evaluation data also deletes manually reported aggregates. Obtain
169
+ consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
170
+ Deleting the connection does not revoke the separate management device key.
171
+ Past conversation text is not erased. Never put record IDs, emails or secrets in
172
+ \`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
173
+
144
174
  ## Boundaries
145
175
 
146
176
  Ask before adding providers or credentials, spending, deploying or external
@@ -1 +1 @@
1
- {"format":1,"contract":"scout-recommend-v1","version":"0.3.4","skill":"---\nname: openmerit-scout\ndescription: Consults OpenMerit Scout before choosing, adding, downloading, replacing or configuring any application inference model, including implicit choices while building features, pretrained browser/local models, SDK defaults, embeddings, OCR, speech, classification, detection, segmentation, LLMs and agents. Not for unrelated edits, ordinary inference calls or this coding agent's own model.\n---\n\n# OpenMerit · Scout\n\nBefore introducing, recommending, replacing or configuring an application\ninference model, call the `scout_models` tool on the `openmerit` MCP server.\nThis applies even when the user never mentions models or Scout. Downloading local\nweights, using a pretrained browser model or accepting an SDK default is a model\ndecision: consult Scout before model-specific code, installs or downloads.\nPreserve the user's explicit model choice; assess it rather than silently replacing\nit. Leave unchanged, approved application selections in place.\n\n## Project policy\n\nRead `.openmerit/scout.json` before searching. If it is absent, ask once whether this\nproject uses **existing-only** (only endpoints it can already reach) or\n**existing-first** (prefer them, ask before searching the whole catalogue), and\nwhich public catalogue endpoint IDs it can access. Save the answer:\n\n```sh\nnpx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...\n```\n\nNever infer access from a model name, SDK or environment-variable name. Never\nrecord credentials, private URLs, prompts or customer data. An empty inventory\nis not permission to search all models.\n\n## Searching\n\nCall `scout_models` with a non-confidential task description as `prompt` and\n`endpointIds` set to the saved inventory, including `[]` when empty. Send\nonly task requirements, never source files, secrets or real customer records.\nIf the MCP tool is unavailable, run\n`npx -y openmerit@latest scout \"<task>\" --json`; it applies the saved inventory.\n\nScout is a conversation with the agent behind openmerit.ai/scout. When\n`status` is `needs_answers`, fill in what the user or project already states,\nshow the user Scout's brief and remaining questions, and wait for their answers.\nThen call again with the returned `conversationId` and `brief` set to the\nrewritten components, or `skip: true` if the user wants Scout to assume. Never\nanswer Scout's questions with guesses. Send later refinements to the same\n`conversationId`. A call with only a `conversationId` reads an interrupted turn.\n\nWhen `status` is `recommended`, Scout has split the work into `tasks`, one\nper model, each with up to five ranked `models`; cover every task. Each\nmodel's `cost` names the `endpointId` and `providerModelId` to call.\nExisting-only never broadens. For existing-first, if no existing candidate fits a\ntask, explain the gap and ask before a new conversation without `endpointIds`.\nCompare quality evidence, reference cost/time and watch-outs. Prefer the cheapest\nusable option the evidence supports, without claiming benchmarks prove\napplication accuracy. Use the project's actual SDK and protocol.\n\n## Measuring candidates\n\nEach task also has an `evaluation`: the checks (per model call, agent run or\nend-to-end outcome), labelled examples per candidate and when to stop a losing\ncandidate. Offer to run it before the user commits to a model. Use the evals the\nproject already has, such as datasets and experiments in its eval or tracing\nplatform, or its own eval scripts and fixtures: add the shortlisted candidates as\nextra runs of those evals. Do not build a second eval or copy their datasets.\nIf the project has no eval for the task, propose a minimal one built from its\nexisting traces and the decisions people already make, and create it only where\nand how the user agrees. Ask before spending on model calls or writing to an\neval platform.\n\nReport what you measured with `scout_models`: the same `conversationId` and\n`results`, one aggregate per model and metric (`level`, `task` title, `scope`,\n`model` endpoint or model ID, `metric`, `value`, `higherIsBetter`,\n`samples`, `source`). Set `kind` to `usd` or `seconds` for measured cost\nor time (per call at the task level, per run at the agent level), so Scout uses\nyour cost instead of its estimate. Report each agent's own checks at the agent\nlevel: an end-to-end result also measures the agents you did not swap. Send\naggregates only, never inputs, outputs or records.\nScout ranks them above public benchmarks for this user only; models whose\n`quality.measured` is true rest on them.\n\n## Boundaries\n\nAsk before adding providers or credentials, spending, deploying or external\nwrites. Report recommendation, endpoint connectivity and application checks as\nseparate results, each only after it succeeds. Do not switch this coding agent's\nown model.\n\nIf Scout fails or has no suitable candidate/evidence for the required deployment,\nsay so and pause the model decision; do not substitute a remembered recommendation\nor SDK default. Continue only model-independent work. Local/browser models are not\nexempt when the catalogue lacks coverage. When an install or login waits for approval (exit code\n10), show the user the approval link and its UTC expiry in your reply, not only in\ncommand output, then rerun the same command once they approve. If it reports that the device key is missing,\nexpired or revoked, run `npx -y openmerit@latest login` and then\n`npx -y openmerit@latest install` again. Never ask the user to paste a key.\n","rule":"# Application model decisions · OpenMerit Scout\n\nBefore choosing, recommending, adding, downloading, replacing or configuring\nANY application inference model, load the `openmerit-scout` skill and consult\n`scout_models` on the `openmerit` MCP server before committing to a model.\nThis applies even when the user only asks you to build a feature, never mentions\nmodels or Scout, or names a model themselves (assess it; do not silently replace it).\n\nA pretrained model, local weights, browser inference, an SDK's default model,\nembeddings, OCR, speech, image classification, detection and segmentation all\ncount as model choices, not implementation details. Identify this decision before\nwriting model-specific code, installing a model package or downloading weights.\n\nFollow the skill's project access policy. If it is missing, ask and save it first.\nScout may answer with clarifying questions: relay them to the user and continue\nthe same Scout conversation with their answers before choosing.\nIf Scout is unavailable, fails or has no suitable evidence/candidate, disclose the\ngap and pause the model decision; do not fall back to a remembered or SDK-default\nmodel. You may continue work that does not commit to a model.\n\nDo not consult Scout for unrelated edits, normal inference calls using an unchanged\napproved model, or this coding agent's own model. Do not reselect approved models\nunless the task introduces or changes a model decision.\n\nWhen the user says \"update openmerit\", run\n`npx -y openmerit@latest update --scope user` (use `--scope project` if this\nrule is in the project's .claude/rules directory). Report the result, including\nany conflict or manual-upgrade requirement. Do not delete edited files, bypass\npermissions or rerun login automatically. A new session loads updated instructions.\n"}
1
+ {"format":1,"contract":"scout-recommend-v2","version":"0.3.5","skill":"---\nname: openmerit-scout\ndescription: Consults OpenMerit Scout before choosing, adding, downloading, replacing or configuring any application inference model, including implicit choices while building features, pretrained browser/local models, SDK defaults, embeddings, OCR, speech, classification, detection, segmentation, LLMs and agents. Not for unrelated edits, ordinary inference calls or this coding agent's own model.\n---\n\n# OpenMerit · Scout\n\nBefore introducing, recommending, replacing or configuring an application\ninference model, call the `scout_models` tool on the `openmerit` MCP server.\nThis applies even when the user never mentions models or Scout. Downloading local\nweights, using a pretrained browser model or accepting an SDK default is a model\ndecision: consult Scout before model-specific code, installs or downloads.\nPreserve the user's explicit model choice; assess it rather than silently replacing\nit. Leave unchanged, approved application selections in place.\n\n## Project policy\n\nRead `.openmerit/scout.json` before searching. If it is absent, ask once whether this\nproject uses **existing-only** (only endpoints it can already reach) or\n**existing-first** (prefer them, ask before searching the whole catalogue), and\nwhich public catalogue endpoint IDs it can access. Save the answer:\n\n```sh\nnpx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...\n```\n\nNever infer access from a model name, SDK or environment-variable name. Never\nrecord credentials, private URLs, prompts or customer data. An empty inventory\nis not permission to search all models.\n\n## Searching\n\nCall `scout_models` with a non-confidential task description as `prompt` and\n`endpointIds` set to the saved inventory, including `[]` when empty. Send\nonly task requirements, never source files, secrets or real customer records.\nIf the MCP tool is unavailable, run\n`npx -y openmerit@latest scout \"<task>\" --json`; it applies the saved inventory.\n\nScout is a conversation with the agent behind openmerit.ai/scout. When\n`status` is `needs_answers`, fill in what the user or project already states,\nshow the user Scout's brief and remaining questions, and wait for their answers.\nThen call again with the returned `conversationId` and `brief` set to the\nrewritten components, or `skip: true` if the user wants Scout to assume. Never\nanswer Scout's questions with guesses. Send later refinements to the same\n`conversationId`. A call with only a `conversationId` reads an interrupted turn.\n\nWhen `status` is `recommended`, Scout has split the work into `tasks`, one\nper model, each with up to five ranked `models`; cover every task. Each\nmodel's `cost` names the `endpointId` and `providerModelId` to call.\nExisting-only never broadens. For existing-first, if no existing candidate fits a\ntask, explain the gap and ask before a new conversation without `endpointIds`.\nCompare quality evidence, reference cost/time and watch-outs. Prefer the cheapest\nusable option the evidence supports, without claiming benchmarks prove\napplication accuracy. Use the project's actual SDK and protocol.\n\n## Measuring candidates\n\nEach task also has an `evaluation`: the checks (per model call, agent run or\nend-to-end outcome), labelled examples per candidate and when to stop a losing\ncandidate. Offer to run it before the user commits to a model. Use the evals the\nproject already has, such as datasets and experiments in its eval or tracing\nplatform, or its own eval scripts and fixtures: add the shortlisted candidates as\nextra runs of those evals. Do not build a second eval or copy their datasets.\nIf the project has no eval for the task, propose a minimal one built from its\nexisting traces and the decisions people already make, and create it only where\nand how the user agrees. Ask before spending on model calls or writing to an\neval platform.\n\nReport what you measured with `scout_models`: the same `conversationId` and\n`results`, one aggregate per model and metric (`level`, `task` title, `scope`,\n`model` endpoint or model ID, `metric`, `value`, `higherIsBetter`,\n`samples`, `source`). Set `kind` to `usd` or `seconds` for measured cost\nor time (per call at the task level, per run at the agent level), so Scout uses\nyour cost instead of its estimate. Report each agent's own checks at the agent\nlevel: an end-to-end result also measures the agents you did not swap. Send\naggregates only, never inputs, outputs or records.\nScout ranks them above public benchmarks for this user only; models whose\n`quality.measured` is true rest on them.\n\nSwap one agent at a time, holding the other agents, prompts, fixtures and controls\nfixed. Use each agent's own checks to attribute failures. If all candidates score\n100%, the eval cannot separate their quality: add harder labelled cases before\nclaiming equivalent quality. Cost may break the observed tie, not prove equality.\n\nIf the user has Langfuse, use `openmerit connect langfuse --help`: the web UI is\noptional. Headless setup requires a separate connection-manager approval with\n`openmerit connect langfuse --authorize --no-open`. Relay its approval link and\nexpiry; after approval, rerun that command. Existing recommendation keys cannot\nmanage connections. The user creates a separate Viewer key; receive it through\nprotected stdin or a secret-manager-injected environment, never chat, tool\narguments, shell literals, traced commands or tracked files. The CLI accepts\nnon-secret settings via `--config mappings.json` and reads LANGFUSE_PUBLIC_KEY\nand LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.\nA separate trusted MCP connection with the management key exposes\n`scout_connection` (status/configure/sync/disconnect/clear). For configure, keys\nmust come from client-controlled HTTP headers outside model context; if the\nclient cannot do this securely, use protected CLI input or the web UI instead.\nAfter they confirm connection and score/model mappings, call `scout_models` with\n`sync: \"langfuse\"` to refresh aggregate measurements before recommending. The CLI\nequivalent is `openmerit scout --conversation <id> --sync langfuse`. Never read\ntrace bodies to work around missing mappings or untagged runs; use the aggregate\nreport path instead. Results are stored per user, used in new conversations and\nexpire after 90 days. Disconnect deletes the Viewer key and imported aggregates;\nDelete all evaluation data also deletes manually reported aggregates. Obtain\nconsent before deleting: headless CLI requires --yes, MCP requires confirm: true.\nDeleting the connection does not revoke the separate management device key.\nPast conversation text is not erased. Never put record IDs, emails or secrets in\n`scope` or `source`; use labels such as \"coding agent\" and \"held-out eval\".\n\n## Boundaries\n\nAsk before adding providers or credentials, spending, deploying or external\nwrites. Report recommendation, endpoint connectivity and application checks as\nseparate results, each only after it succeeds. Do not switch this coding agent's\nown model.\n\nIf Scout fails or has no suitable candidate/evidence for the required deployment,\nsay so and pause the model decision; do not substitute a remembered recommendation\nor SDK default. Continue only model-independent work. Local/browser models are not\nexempt when the catalogue lacks coverage. When an install or login waits for approval (exit code\n10), show the user the approval link and its UTC expiry in your reply, not only in\ncommand output, then rerun the same command once they approve. If it reports that the device key is missing,\nexpired or revoked, run `npx -y openmerit@latest login` and then\n`npx -y openmerit@latest install` again. Never ask the user to paste a key.\n","rule":"# Application model decisions · OpenMerit Scout\n\nBefore choosing, recommending, adding, downloading, replacing or configuring\nANY application inference model, load the `openmerit-scout` skill and consult\n`scout_models` on the `openmerit` MCP server before committing to a model.\nThis applies even when the user only asks you to build a feature, never mentions\nmodels or Scout, or names a model themselves (assess it; do not silently replace it).\n\nA pretrained model, local weights, browser inference, an SDK's default model,\nembeddings, OCR, speech, image classification, detection and segmentation all\ncount as model choices, not implementation details. Identify this decision before\nwriting model-specific code, installing a model package or downloading weights.\n\nFollow the skill's project access policy. If it is missing, ask and save it first.\nScout may answer with clarifying questions: relay them to the user and continue\nthe same Scout conversation with their answers before choosing.\nIf Scout is unavailable, fails or has no suitable evidence/candidate, disclose the\ngap and pause the model decision; do not fall back to a remembered or SDK-default\nmodel. You may continue work that does not commit to a model.\n\nDo not consult Scout for unrelated edits, normal inference calls using an unchanged\napproved model, or this coding agent's own model. Do not reselect approved models\nunless the task introduces or changes a model decision.\n\nWhen the user says \"update openmerit\", run\n`npx -y openmerit@latest update --scope user` (use `--scope project` if this\nrule is in the project's .claude/rules directory). Report the result, including\nany conflict or manual-upgrade requirement. Do not delete edited files, bypass\npermissions or rerun login automatically. A new session loads updated instructions.\n"}
@@ -0,0 +1,9 @@
1
+ export function browserCommand(url: any, platform: NodeJS.Platform | undefined, env: NodeJS.ProcessEnv | undefined, allowed: any): (string | any[])[] | null;
2
+ export function openBrowser(url: any, { platform, env, spawnProcess, timeoutMs, allowed }?: {
3
+ platform?: NodeJS.Platform | undefined;
4
+ env?: NodeJS.ProcessEnv | undefined;
5
+ spawnProcess?: typeof spawn | undefined;
6
+ timeoutMs?: number | undefined;
7
+ allowed?: ((url: string) => boolean) | undefined;
8
+ }): Promise<any>;
9
+ import { spawn } from 'node:child_process';
@@ -10,7 +10,7 @@ export function browserCommand(url, platform = process.platform, env = process.e
10
10
  return null;
11
11
  }
12
12
 
13
- export async function openBrowser(url, { platform = process.platform, env = process.env, spawnProcess = spawn, timeoutMs = 3000, allowed } = {}) {
13
+ export async function openBrowser(url, { platform = process.platform, env = process.env, spawnProcess = spawn, timeoutMs = 3000, allowed = /** @type {(url: string) => boolean} */ (() => false) } = {}) {
14
14
  const command = browserCommand(url, platform, env, allowed);
15
15
  if (!command) return false;
16
16
  return new Promise(resolve => {
@@ -6,11 +6,14 @@ const help = `OpenMerit
6
6
  openmerit scout --help Ask Scout which models fit your tasks
7
7
  openmerit install --help Connect OpenMerit to your coding agent
8
8
  openmerit update --help Update installed Scout instructions
9
+ openmerit connect langfuse --help Connect through CLI or the optional web UI
10
+ openmerit disconnect langfuse Delete the connection and imports
9
11
  openmerit --help Show this help
10
12
  `;
11
13
 
12
14
  async function main() {
13
15
  const args = process.argv.slice(2);
16
+ if (['connect', 'disconnect'].includes(args[0])) { const { connection } = await import('./connections.js'); process.exitCode = await connection(args); return; }
14
17
  if (['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor', 'update'].includes(args[0])) { const { main } = await import('./install.mjs'); process.exitCode = await main(args); return; }
15
18
  if (args[0] === 'scout') { const { main } = await import('./scout.js'); process.exitCode = await main(args.slice(1)); return; }
16
19
  if (!args.length || args.includes('--help') || args.includes('-h')) { console.log(help); return; }
@@ -0,0 +1,145 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { credentialsPath, readCredentials } from "./scout-credentials.js";
4
+ import { openBrowser } from './browser.mjs';
5
+ import { main as install } from './install.mjs';
6
+ const help = `OpenMerit · Langfuse connections
7
+
8
+ openmerit connect langfuse Open the optional web UI
9
+ openmerit connect langfuse --authorize [--no-open] [--json]
10
+ openmerit connect langfuse --config mappings.json [--json]
11
+ openmerit connect langfuse --stdin [--json]
12
+ openmerit connect langfuse --status|--sync [--json]
13
+ openmerit disconnect langfuse --yes [--json]
14
+ openmerit disconnect langfuse --delete-all --yes [--json]
15
+
16
+ Authorize a separate connection-manager key once. First-time approval still
17
+ requires account sign-in; --no-open prints a link for approval on any device.
18
+ Rerun --authorize if pending (exit 10). This never replaces recommendation keys.
19
+ The management key is saved separately with owner-only file permissions.
20
+
21
+ --config reads non-secret region, from (YYYY-MM-DD), tagPrefix, mappings and models.
22
+ Supply LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY through a secret manager.
23
+ --stdin reads the full JSON settings including the Viewer key pair from a protected
24
+ pipe (maximum 18 KB), never echoes them, and does not save them locally.
25
+ Never put keys in arguments, chat, shell history or a tracked file. Use a separate
26
+ Viewer key, not tracing keys. OPENMERIT_CONNECTION_KEY can supply an already
27
+ approved management key to unattended jobs; otherwise the saved key is used.
28
+
29
+ Disconnect deletes the key pair and imports, not reported results or conversations.
30
+ --delete-all also removes reported results. Both require --yes for headless use.
31
+ Without --yes, disconnect opens the optional browser confirmation.
32
+ `;
33
+ const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
34
+ async function settings(file, stdin, root) {
35
+ let text = '';
36
+ if (stdin) {
37
+ if (process.stdin.isTTY)
38
+ throw new Error('--stdin requires a protected JSON pipe, not interactive key entry.');
39
+ process.stdin.setEncoding('utf8');
40
+ let bytes = 0;
41
+ for await (const chunk of process.stdin) {
42
+ bytes += Buffer.byteLength(chunk);
43
+ if (bytes > 18_000)
44
+ throw new Error('Connection settings exceed 18 KB.');
45
+ text += String(chunk);
46
+ }
47
+ }
48
+ else {
49
+ try {
50
+ text = await readFile(resolve(root, file ?? ''), 'utf8');
51
+ }
52
+ catch {
53
+ throw new Error('Could not read connection settings.');
54
+ }
55
+ }
56
+ if (Buffer.byteLength(text) > 18_000)
57
+ throw new Error('Connection settings exceed 18 KB.');
58
+ let config;
59
+ try {
60
+ config = JSON.parse(text);
61
+ }
62
+ catch {
63
+ throw new Error('Connection settings must be a JSON object.');
64
+ }
65
+ const fields = ['region', 'from', 'tagPrefix', 'mappings', 'models', ...(stdin ? ['publicKey', 'secretKey'] : [])];
66
+ if (!object(config) || Object.keys(config).some(key => !fields.includes(key)))
67
+ throw new Error('Use non-secret settings in --config; credentials must come from a secret manager or --stdin.');
68
+ if (!stdin)
69
+ config = { ...config, publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY };
70
+ if (!object(config) || typeof config.publicKey !== 'string' || typeof config.secretKey !== 'string' || !config.publicKey || !config.secretKey)
71
+ throw new Error('Supply a separate Viewer key pair through a secret manager or --stdin.');
72
+ return config;
73
+ }
74
+ export async function connection(args, { root = process.cwd(), print = console.log, printError = console.error } = {}) {
75
+ if (args.includes('--help') || args.includes('-h')) {
76
+ print(help);
77
+ return 0;
78
+ }
79
+ const [command, provider, ...flags] = args;
80
+ if (!['connect', 'disconnect'].includes(command ?? '') || provider !== 'langfuse')
81
+ throw new Error('Use openmerit connect langfuse --help.');
82
+ let file;
83
+ const options = new Set();
84
+ for (let i = 0; i < flags.length; i++) {
85
+ const flag = flags[i] ?? '';
86
+ if (options.has(flag))
87
+ throw new Error('Duplicate connection option.');
88
+ options.add(flag);
89
+ if (flag === '--config') {
90
+ file = flags[++i];
91
+ if (!file || file.startsWith('-'))
92
+ throw new Error('--config needs a settings file.');
93
+ }
94
+ else if (!['--stdin', '--authorize', '--no-open', '--status', '--sync', '--yes', '--delete-all', '--json'].includes(flag))
95
+ throw new Error('Unknown connection option. Use --help; never pass keys as arguments.');
96
+ }
97
+ const operation = ['--config', '--stdin', '--authorize', '--status', '--sync'].filter(flag => options.has(flag));
98
+ if (operation.length > 1 || command === 'disconnect' && operation.length || command === 'connect' && (options.has('--yes') || options.has('--delete-all'))
99
+ || options.has('--no-open') && !options.has('--authorize'))
100
+ throw new Error('Choose one connection action. Use --help.');
101
+ if (options.has('--delete-all') && !options.has('--yes'))
102
+ throw new Error('--delete-all requires --yes.');
103
+ if (options.has('--authorize'))
104
+ return install(['login', '--client', 'connector', '--scope', 'user', ...(options.has('--no-open') ? ['--no-open'] : []), ...(options.has('--json') ? ['--json'] : [])], { root, print, printError });
105
+ if (!operation.length && !options.has('--yes')) {
106
+ const url = 'https://openmerit.ai/connect/?integration=langfuse';
107
+ print(`Manage Langfuse in your signed-in browser: ${url}`);
108
+ await openBrowser(url, { allowed: (value) => value === url }).catch(() => false);
109
+ return 0;
110
+ }
111
+ const server = (process.env.OPENMERIT_URL ?? 'https://openmerit.ai').replace(/\/+$/, '');
112
+ const url = new URL(server);
113
+ if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))))
114
+ throw new Error('Use an HTTPS OpenMerit origin, or localhost for development.');
115
+ const credentials = readCredentials(credentialsPath('connector', 'user', root));
116
+ const key = process.env.OPENMERIT_CONNECTION_KEY ?? (credentials?.server === server ? credentials.key : undefined);
117
+ if (!key || !/^om_[A-Za-z0-9_-]{43}$/.test(key))
118
+ throw new Error('Authorize connection management first: openmerit connect langfuse --authorize --no-open');
119
+ // Read secrets only after authorization. No credential values reach stdout,
120
+ // local files, tool arguments, analytics or errors, even on malformed input.
121
+ const config = file || options.has('--stdin') ? await settings(file, options.has('--stdin'), root) : undefined;
122
+ const path = options.has('--delete-all') ? 'evaluations' : options.has('--sync') ? 'langfuse/sync' : 'langfuse';
123
+ const method = command === 'disconnect' ? 'DELETE' : config || options.has('--sync') ? 'POST' : 'GET';
124
+ let response;
125
+ try {
126
+ response = await fetch(`${server}/connect/api/${path}`, { method, redirect: 'error', signal: AbortSignal.timeout(120_000),
127
+ headers: { Authorization: `Bearer ${key}`, ...(config ? { 'Content-Type': 'application/json' } : {}), 'X-Scout-Analytics': 'off' },
128
+ ...(config ? { body: JSON.stringify(config) } : {}),
129
+ });
130
+ }
131
+ catch {
132
+ throw new Error('Could not reach OpenMerit. Check connection status before retrying a change.');
133
+ }
134
+ if (!response.ok) {
135
+ await response.body?.cancel();
136
+ throw new Error(response.status === 401 || response.status === 403 ? 'Management key rejected. Run openmerit connect langfuse --authorize --no-open.'
137
+ : `Connection request failed (HTTP ${response.status}). Check Viewer permissions, region and mappings; check status before retrying.`);
138
+ }
139
+ const value = await response.json();
140
+ if (!object(value))
141
+ throw new Error('OpenMerit returned an invalid connection response.');
142
+ const state = Object.fromEntries(Object.entries(value).filter(([name]) => ['connected', 'connectedAt', 'syncedAt', 'experiments', 'unmapped', 'results', 'busy', 'cached', 'deleted'].includes(name)));
143
+ print(JSON.stringify(state, null, 2));
144
+ return 0;
145
+ }
@@ -0,0 +1,2 @@
1
+ export function main(args: any, dependencies?: {}): Promise<any>;
2
+ export { rule, skill } from "./scout-instructions.ts";
@@ -19,7 +19,7 @@ const exitCodes = { ok: 0, error: 1, pending: 10, denied: 11, conflict: 20, unre
19
19
  const exec = promisify(execFile);
20
20
  const hash = value => createHash('sha256').update(value).digest('hex');
21
21
  const policyPath = '.openmerit/scout.json';
22
- const clients = { 'claude-code': 'Claude Code', pi: 'Pi' };
22
+ const clients = { 'claude-code': 'Claude Code', pi: 'Pi', connector: 'OpenMerit connection manager' };
23
23
 
24
24
  const help = `OpenMerit · coding-agent setup
25
25
 
@@ -45,6 +45,8 @@ It opens the approval page and waits up to 9 minutes for the Approve click
45
45
  pending, rerun the same command: it resumes the same request.
46
46
  --client defaults to the coding agent running the command. --scope defaults to user.
47
47
  --plan shows the changes without making them. Keys are never printed.
48
+ login/logout also accept --client connector for a separate connection-management
49
+ key. This grants no model recommendations and changes no coding-agent install.
48
50
 
49
51
  Exit codes: 0 done, 10 approval pending, 11 denied or expired, 20 conflict,
50
52
  30 OpenMerit unreachable, 1 other error.
@@ -471,6 +473,7 @@ async function runMain(args, { root = process.cwd(), print = console.log, printE
471
473
  options.client ??= detectClient() ?? (['install', 'status', 'remove'].includes(action) ? undefined : 'claude-code');
472
474
  if (!options.client) throw new Error('Choose the coding agent with --client claude-code or --client pi.');
473
475
  if (!clients[options.client]) throw new Error(`Unsupported client "${options.client}". Supported: ${Object.keys(clients).join(', ')}.`);
476
+ if (options.client === 'connector' && !['login', 'logout'].includes(action)) throw new Error('The connector client supports login/logout only. Use openmerit connect langfuse --help.');
474
477
  }
475
478
  root = resolve(root);
476
479
  telemetry.root = root;
@@ -510,7 +513,7 @@ async function runMain(args, { root = process.cwd(), print = console.log, printE
510
513
  : action === 'project' ? await project(root, options)
511
514
  : action === 'doctor' ? await doctor(root, options)
512
515
  : action === 'logout' ? { action, revoked: await revokeKey(telemetry), note: 'Signed out. This device key was revoked and deleted. Agents configured with it stop working until you run install again.' }
513
- : (await revokeKey(telemetry), await login({ client: options.client, scope: options.scope, open: options.open, log, wait, telemetry }), { action, signedIn: true, note: `Signed in. Run "openmerit install --client ${options.client} --scope ${options.scope}" to refresh this installation.` });
516
+ : (await revokeKey(telemetry), await login({ client: options.client, scope: options.scope, open: options.open, log, wait, telemetry }), { action, signedIn: true, note: options.client === 'connector' ? 'Connection management authorized. Recommendation keys and coding-agent installations are unchanged.' : `Signed in. Run "openmerit install --client ${options.client} --scope ${options.scope}" to refresh this installation.` });
514
517
  if (result.conflicts?.length && telemetry.outcome !== 'error') { telemetry.outcome = 'conflict'; telemetry.properties.code = 'install_conflict'; }
515
518
  if (options.plan) await telemetry.step(telemetry.stage, telemetry.outcome, { applied: false });
516
519
  print(options.json ? JSON.stringify({ ok: action !== 'update' || !['conflict', 'retry', 'manual_required'].includes(result.status), ...result }, null, 2) : render(result));
@@ -107,6 +107,36 @@ aggregates only, never inputs, outputs or records.
107
107
  Scout ranks them above public benchmarks for this user only; models whose
108
108
  \`quality.measured\` is true rest on them.
109
109
 
110
+ Swap one agent at a time, holding the other agents, prompts, fixtures and controls
111
+ fixed. Use each agent's own checks to attribute failures. If all candidates score
112
+ 100%, the eval cannot separate their quality: add harder labelled cases before
113
+ claiming equivalent quality. Cost may break the observed tie, not prove equality.
114
+
115
+ If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
116
+ optional. Headless setup requires a separate connection-manager approval with
117
+ \`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
118
+ expiry; after approval, rerun that command. Existing recommendation keys cannot
119
+ manage connections. The user creates a separate Viewer key; receive it through
120
+ protected stdin or a secret-manager-injected environment, never chat, tool
121
+ arguments, shell literals, traced commands or tracked files. The CLI accepts
122
+ non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
123
+ and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
124
+ A separate trusted MCP connection with the management key exposes
125
+ \`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
126
+ must come from client-controlled HTTP headers outside model context; if the
127
+ client cannot do this securely, use protected CLI input or the web UI instead.
128
+ After they confirm connection and score/model mappings, call \`scout_models\` with
129
+ \`sync: "langfuse"\` to refresh aggregate measurements before recommending. The CLI
130
+ equivalent is \`openmerit scout --conversation <id> --sync langfuse\`. Never read
131
+ trace bodies to work around missing mappings or untagged runs; use the aggregate
132
+ report path instead. Results are stored per user, used in new conversations and
133
+ expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
134
+ Delete all evaluation data also deletes manually reported aggregates. Obtain
135
+ consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
136
+ Deleting the connection does not revoke the separate management device key.
137
+ Past conversation text is not erased. Never put record IDs, emails or secrets in
138
+ \`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
139
+
110
140
  ## Boundaries
111
141
 
112
142
  Ask before adding providers or credentials, spending, deploying or external
@@ -12,6 +12,9 @@ const help = `OpenMerit · Scout
12
12
  openmerit scout "<the work models need to do>" [--all-endpoints] [--json]
13
13
  openmerit scout --conversation <id> [--brief <component>=<text> ...] [--skip] ["<follow-up>"] [--json]
14
14
  openmerit scout --conversation <id> --results <file.json> [--json]
15
+ openmerit scout --conversation <id> --sync langfuse [--json]
16
+ openmerit connect langfuse
17
+ openmerit disconnect langfuse
15
18
 
16
19
  Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
17
20
  brief with clarifying questions: answer them with --brief (components: input,
@@ -24,6 +27,13 @@ samples, source}), never inputs or outputs. kind is quality (the default), usd
24
27
  or seconds; measured usd replaces Scout's cost estimate. Scout ranks them above
25
28
  public benchmarks, for your recommendations only. A turn usually takes one to three minutes.
26
29
 
30
+ Connect a separate Langfuse Viewer key through CLI or the optional web UI (see
31
+ openmerit connect langfuse --help), then --sync langfuse refreshes mapped, tagged
32
+ experiment aggregates before recommending.
33
+ Results are stored per user across conversations for 90 days. Disconnect deletes
34
+ the key and imports; use Delete all evaluation data to remove reported results too.
35
+ Past conversations remain. Never put record IDs or emails in source or scope.
36
+
27
37
  A new conversation is limited to the endpoint IDs in ${policyFile}
28
38
  (set with openmerit project). --all-endpoints searches the whole catalogue and is
29
39
  refused under the existing-only policy. Uses the device key that
@@ -177,6 +187,11 @@ export async function main(args, { root = process.cwd(), print = console.log } =
177
187
  request.allEndpoints = true;
178
188
  else if (arg === '--json')
179
189
  json = true;
190
+ else if (arg === '--sync') {
191
+ if (value() !== 'langfuse')
192
+ throw new Error('--sync supports langfuse.');
193
+ request.sync = 'langfuse';
194
+ }
180
195
  else if (arg === '--results') {
181
196
  const file = value();
182
197
  let results;
@@ -190,7 +205,9 @@ export async function main(args, { root = process.cwd(), print = console.log } =
190
205
  throw new Error(`${file} must hold a JSON list of results.`);
191
206
  // Scout accepts at most 50 per turn; each call is a turn that recommends again, so splitting is the caller's choice.
192
207
  if (results.length > 50)
193
- throw new Error(`${file} holds ${results.length} results; Scout accepts at most 50 per call (200 per conversation). Split them into files of up to 50 and report each with --results in turn; Scout recommends again after each.`);
208
+ throw new Error(`${file} holds ${results.length} results; Scout accepts at most 50 per call. Split them into files of up to 50 and report each with --results in turn; Scout recommends again after each.`);
209
+ if (results.some(r => isObject(r) && [r.source, r.scope].some(v => typeof v === 'string' && /(?:\b(?:claim|patient|member|FICT|CLM)[-_ ]?\d|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|[^\s@]+@[^\s@]+\.[^\s@]+)/i.test(v))))
210
+ print('Warning: source or scope looks like a record ID or email. Send aggregate labels only; remove identifiers before sharing.');
194
211
  request.results = results;
195
212
  }
196
213
  else if (arg.startsWith('-'))
@@ -0,0 +1,30 @@
1
+ export function safeFrames(error: any): {
2
+ filename: string | undefined;
3
+ lineno: number;
4
+ colno: number;
5
+ }[];
6
+ export function errorProperties(error: any): {
7
+ error_type: any;
8
+ frames: {
9
+ filename: string | undefined;
10
+ lineno: number;
11
+ colno: number;
12
+ }[];
13
+ };
14
+ export function setupHeaders(ids?: {}, client?: string): {
15
+ 'X-Scout-Installation-Id'?: any;
16
+ 'X-Scout-Setup-Id'?: any;
17
+ 'X-Scout-Analytics-Account'?: string;
18
+ 'X-Scout-Analytics'?: string;
19
+ 'X-Scout-Analytics-Content': string;
20
+ 'X-Scout-Client-Version'?: any;
21
+ 'X-Scout-Client': string;
22
+ 'X-Scout-Agent': string;
23
+ };
24
+ export function installationProperties(input: any): {} | null;
25
+ export function sendInstallationEvent(input: any): Promise<void>;
26
+ export function commandTelemetry(args: any): (outcome: any, error: any, code: any) => Promise<void>;
27
+ export let clientVersion: any;
28
+ export function telemetryEnabled(defaultEnabled?: boolean): boolean;
29
+ export function setupCorrelationEnabled(): boolean;
30
+ export function setupUUID(value: any): boolean;
@@ -7,7 +7,7 @@ import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/prom
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { gunzipSync } from 'node:zlib';
10
- export const updateContract = 'scout-recommend-v1';
10
+ export const updateContract = 'scout-recommend-v2';
11
11
  const registry = 'https://registry.npmjs.org/openmerit/latest';
12
12
  const day = 24 * 60 * 60 * 1000;
13
13
  const retry = 15 * 60 * 1000;
package/docs/index.md CHANGED
@@ -16,6 +16,9 @@ Follow [setup](getting-started.md#connect-scout-to-a-coding-agent) and [the Pi g
16
16
 
17
17
  Use your own representative task checks to evaluate model candidates. The [standalone evaluation CLI is retired in npm 0.3.0](agent-evaluations.md); existing local artifacts are not deleted.
18
18
 
19
+ Follow [close the model-selection loop](scout-close-loop.md) to consult, measure,
20
+ report and choose. It distinguishes npm 0.3.4 from the unreleased Langfuse connector.
21
+
19
22
  ## Operate and troubleshoot
20
23
 
21
24
  [Commands](commands.md) lists the Scout and setup commands. The [Inference Terminal is retired in npm 0.3.0](inference-terminal.md); existing local files are not deleted. [Troubleshooting](troubleshooting.md), [security and data](security.md), and [current limitations](roadmap.md) explain setup failures and product boundaries. [Testing and validation](testing.md) distinguishes automated coverage from product evidence.
@@ -0,0 +1,258 @@
1
+ # Close the model-selection loop
2
+
3
+ Scout recommends candidates; your own evals decide whether they work. Never put
4
+ inputs, outputs, patient records, source code or secrets in recommendation prompts
5
+ or tool arguments. Connector credentials use the protected paths described below.
6
+
7
+ **Version requirements:** the Langfuse setup commands below require npm 0.3.5 or
8
+ newer and a matching Scout serving deployment. Saving Langfuse credentials also
9
+ requires connection encryption to be enabled on the server. Publishing the npm
10
+ package alone does not deploy or enable the hosted connector. npm 0.3.4 supports
11
+ reporting aggregates per conversation, not this connector.
12
+
13
+ ## Install and consult
14
+
15
+ Install the published package, approve the device code in your browser and start a
16
+ new coding-agent session. See [setup](getting-started.md) for Pi and unattended
17
+ reuse of an already-approved device key. Do not copy another user's key.
18
+
19
+ ```sh
20
+ npx -y openmerit@latest install --client claude-code --scope project
21
+ npx -y openmerit@latest scout "Extract invoice line items as structured JSON" --all-endpoints
22
+ ```
23
+
24
+ Use your saved project inventory instead of `--all-endpoints` when policy limits
25
+ access. Existing-only policy cannot be broadened by that flag. Answer Scout's
26
+ questions in the same conversation with `--brief`, or explicitly choose `--skip`.
27
+ Each recommended model names an exact endpoint and provider model ID.
28
+
29
+ ## Measure with your own evals
30
+
31
+ Add the shortlist as runs of your existing evals, not a second platform. Obtain
32
+ approval before model spend or platform writes. For agents, change **one agent at
33
+ a time** while holding the other models, prompts, tools, fixtures and controls
34
+ fixed. Use that agent's own checks, not just end-to-end success, to attribute
35
+ failures. Include cost and latency per call or agent run.
36
+
37
+ If every candidate scores 100%, the eval does not distinguish their quality.
38
+ Add harder labelled boundary cases, rare failures and adversarial examples;
39
+ cheaper on this test is not proof of equal quality in production.
40
+
41
+ ## Report aggregates
42
+
43
+ Report up to 50 aggregates per call, one per model, metric and scope. Use public
44
+ catalogue model or endpoint IDs. `scope` and `source` must be aggregate labels,
45
+ not record IDs, emails or URLs containing private identifiers.
46
+
47
+ ```json
48
+ [
49
+ {
50
+ "level": "agent", "kind": "quality", "task": "Invoice extraction",
51
+ "scope": "Invoice extraction", "model": "<catalogue-model-id>",
52
+ "metric": "fields-correct", "value": 0.96, "higherIsBetter": true,
53
+ "samples": 100, "source": "Held-out invoice eval"
54
+ }
55
+ ]
56
+ ```
57
+
58
+ ```sh
59
+ openmerit scout --conversation <id> --results results.json
60
+ ```
61
+
62
+ Use `kind: "usd"` or `"seconds"` for measured money or time; they are not quality
63
+ scores. Once the per-user change is deployed, aggregates apply across your
64
+ conversations. Re-reporting the same level/kind/task/scope/model/metric/source
65
+ updates that aggregate rather than duplicating it.
66
+
67
+ ## Connect Langfuse
68
+
69
+ After release, `openmerit connect langfuse` opens the optional signed-in
70
+ [connection page](https://openmerit.ai/connect/?integration=langfuse). Create a
71
+ **separate Viewer key pair** in Langfuse Project Settings → API Keys. Enter it
72
+ in the password fields there, or use the protected headless paths below. Never
73
+ paste it in chat, command arguments, issues or tracked files. Keep the
74
+ application's tracing keys unchanged. US, EU and HIPAA Cloud hosts
75
+ are supported; self-hosted instances are not. Selecting a Langfuse region does
76
+ not promise that Scout stores or processes your data in that region.
77
+
78
+ You can connect the key first with empty mappings. To import results, expand
79
+ **Score and model mappings** and supply a start date, experiment-tag prefix,
80
+ score mappings and model aliases. To change saved settings, disconnect and
81
+ reconnect; this deletes the old imports, not reported results.
82
+
83
+ ### Headless CLI setup
84
+
85
+ First authorize a **separate connection-management key**. This is not an upgrade
86
+ to your existing recommendation key and does not change Claude Code or Pi setup.
87
+ The first account authorization still requires a signed-in person to approve the
88
+ displayed device code; `--no-open` lets them do that on another device. After
89
+ approval, all connector operations can run without the web UI.
90
+
91
+ ```sh
92
+ openmerit connect langfuse --authorize --no-open
93
+ ```
94
+
95
+ If it returns exit 10 (approval pending), approve the matching code and rerun the
96
+ same command. The resumable login stores the management key with mode 0600 in
97
+ `connector/user/credentials.json` under the OpenMerit config directory (normally
98
+ `~/.config/openmerit`). Never display or share that file. The key grants
99
+ `scout:connections`, not `scout:recommend`; it can manage **all evaluation data
100
+ for your account**, not only one project. It cannot approve devices, list other
101
+ device keys, run recommendations or spend on models. Like recommendation keys,
102
+ it expires after 90 days without use and can be revoked from the connection page
103
+ or with `openmerit logout --client connector`. Disconnecting Langfuse does not
104
+ revoke this management credential; revoke it separately when retiring a runner.
105
+
106
+ Save only non-secret settings in `mappings.json`:
107
+
108
+ ```json
109
+ {
110
+ "region": "us",
111
+ "from": "2026-11-01",
112
+ "tagPrefix": "experiment:",
113
+ "mappings": [],
114
+ "models": {}
115
+ }
116
+ ```
117
+
118
+ Have your secret manager inject `LANGFUSE_PUBLIC_KEY` and `LANGFUSE_SECRET_KEY`
119
+ into the CLI process environment, then run:
120
+
121
+ ```sh
122
+ openmerit connect langfuse --config mappings.json
123
+ openmerit connect langfuse --status
124
+ openmerit connect langfuse --sync
125
+ ```
126
+
127
+ Alternatively, `openmerit connect langfuse --stdin` reads a full JSON object with
128
+ the same settings plus `publicKey` and `secretKey` from a protected pipe, up to
129
+ 18 KB. It does not echo or save the Viewer credentials locally. Do not use inline
130
+ JSON, `echo` with literal secrets, command substitutions that print secrets into
131
+ an agent transcript, or an interactive shell history entry. Authorize before
132
+ piping a secret. Environment variables and pipes are not a vault: trust the
133
+ runner and restrict process/debug access, and disable shell tracing and request
134
+ logging. Unattended jobs may instead inject an already-approved
135
+ `OPENMERIT_CONNECTION_KEY` through a secret manager; keep recommendation and
136
+ management credentials separate.
137
+
138
+ ### MCP management
139
+
140
+ The canonical `/scout/mcp` endpoint exposes `scout_connection` when authenticated
141
+ with the separate management key (or a current browser session). Register a
142
+ separate trusted MCP client connection using `Authorization: Bearer <management
143
+ key>` without putting the actual key in chat or tool arguments. A recommendation
144
+ key still discovers only `scout_models`; a management key discovers only
145
+ `scout_connection`. This separation is enforced on calls, not just discovery.
146
+
147
+ `scout_connection` supports `status`, `configure`, `sync`, `disconnect` and
148
+ `clear`. `configure` takes the non-secret settings above as `config`. Supply the
149
+ Viewer key pair through **client-controlled HTTP headers**
150
+ `X-Langfuse-Public-Key` and `X-Langfuse-Secret-Key`, sourced from your secret manager,
151
+ not from the model. These headers are consumed only for configuration and are
152
+ never returned in tool results or sent to the recommendation model. Remove them
153
+ from client configuration after connecting. Do not configure a client/proxy that
154
+ records these headers. If your MCP client cannot securely inject headers outside
155
+ model context, have the agent invoke the CLI with protected input instead, or
156
+ use the web UI; do not fall back to putting credentials in tool arguments.
157
+
158
+ ```json
159
+ {"action": "configure", "config": {"region": "us", "from": "2026-11-01", "tagPrefix": "experiment:", "mappings": [], "models": {}}}
160
+ ```
161
+
162
+ This checks and saves the connection, not a sync. Send `{"action":"sync"}` to
163
+ import mapped aggregates without starting inference. For deletion, obtain the
164
+ user's consent and pass `confirm: true` with `disconnect` or `clear`. Tool
165
+ confirmation is not a substitute for consent in your application.
166
+
167
+ ### HTTP API
168
+
169
+ The same authenticated account store is available without MCP:
170
+
171
+ | Method | Path | Operation |
172
+ | --- | --- | --- |
173
+ | GET | `/connect/api/langfuse` | Connection status and aggregate counts, never credentials |
174
+ | POST | `/connect/api/langfuse` | Configure/replace using full JSON settings including `publicKey` and `secretKey` |
175
+ | POST | `/connect/api/langfuse/sync` | Import mapped aggregates without inference |
176
+ | DELETE | `/connect/api/langfuse` | Delete the Viewer key pair and imported aggregates |
177
+ | DELETE | `/connect/api/evaluations` | Also delete manually reported aggregates |
178
+
179
+ Send the management key in the `Authorization: Bearer` header and
180
+ `Content-Type: application/json` for configuration. Browser session tokens still
181
+ work. Recommendation keys, anonymous requests and cross-origin browser requests
182
+ are rejected. No request accepts a target user ID: ownership comes from the
183
+ authenticated identity. Inject credentials into the HTTP client, not command-line
184
+ arguments; keep request bodies/headers out of logs. Check status after an ambiguous
185
+ network failure before retrying a change. None of these paths bypasses the Viewer
186
+ check, explicit mappings, encryption, retention limits or per-user isolation.
187
+
188
+ ### Attribute imported scores
189
+
190
+ Every scored trace must carry a tag formed from the prefix plus its experiment
191
+ ID, for example `experiment:<id>`. The experiment's metadata must identify the
192
+ model for that agent. Scout does not infer model attribution from timestamps,
193
+ trace contents or the currently selected model. Untagged historical runs are
194
+ not imported: report their aggregates instead. Each mapping specifies an exact
195
+ trace name so other agents in the same experiment cannot contaminate its score.
196
+
197
+ Example score mapping (replace the task title with the exact Scout task):
198
+
199
+ ```json
200
+ [
201
+ {
202
+ "metric": "coding-finding-found", "modelKey": "model_coding",
203
+ "task": "Coding review", "traceName": "review-coding",
204
+ "level": "agent", "kind": "quality", "higherIsBetter": true
205
+ }
206
+ ]
207
+ ```
208
+
209
+ Model aliases explicitly map metadata values to Scout model or endpoint IDs:
210
+
211
+ ```json
212
+ {"<experiment metadata model value>": "<Scout catalogue model or endpoint ID>"}
213
+ ```
214
+
215
+ The first import supports numeric/boolean score averages and counts, not strings,
216
+ raw scores, percentile distributions or automatic cost/latency extraction from
217
+ generation bodies. At most eight mappings, 100 aliases and 100 experiments are
218
+ read per sync. Narrow the date if a response or import exceeds its limit. Only
219
+ experiment headers/metadata and aggregate Metrics v2 reads are requested. A
220
+ deliberately invalid dataset-create request checks read-only permission without
221
+ ever sending a valid write payload. Only an authenticated read followed by HTTP
222
+ 403 on that probe is accepted; other responses fail closed. Actual Viewer-key
223
+ acceptance remains pending the release check.
224
+
225
+ Click **Sync aggregates**, or request a fresh recommendation with:
226
+
227
+ ```sh
228
+ openmerit scout --conversation <id> --sync langfuse
229
+ ```
230
+
231
+ MCP uses `sync: "langfuse"`. Successful reads are cached for ten minutes; a
232
+ failed read cannot be retried for one minute. A failed import leaves the previous
233
+ complete set intact. `conversationId` alone reads a saved answer, not a rerank;
234
+ send a follow-up or `sync` to recompute with the current results.
235
+
236
+ ## Choose and delete
237
+
238
+ Check measured quality, the quality floor, price calibration and limitations
239
+ before adopting a model. Benchmark estimates are not application acceptance.
240
+ Test endpoint connectivity and application behavior separately.
241
+
242
+ Results belong only to your Scout user account, not other customers. Up to 5,000
243
+ aggregates or 1 MB are retained for 90 days from report/import; expired rows are
244
+ excluded immediately and pruned daily (allow scheduling delay). An evaluation
245
+ account inactive for 90 days is erased by the same cleanup. See the
246
+ [data policy](scout-data-policy.md) for processors and backup limitations.
247
+
248
+ `openmerit disconnect langfuse` opens the optional browser confirmation;
249
+ `openmerit disconnect langfuse --yes` deletes headlessly, and adding
250
+ `--delete-all` also deletes reported results. **Disconnect &
251
+ delete imports** removes the encrypted key pair and imported aggregates, including
252
+ an in-flight sync. **Delete all evaluation data** also removes manually reported
253
+ aggregates. Neither erases past conversation text or previously generated
254
+ recommendations; do not keep treating those answers as current measurements.
255
+ Contact the address in the policy for broader deletion.
256
+
257
+ This release changes the installed instruction-data contract. Existing automatic
258
+ updaters stop at that boundary; explicitly reinstall to adopt the new instructions.
@@ -25,6 +25,53 @@ Setup analytics defaults on. Set `OPENMERIT_TELEMETRY=0` or `DO_NOT_TRACK=1` to
25
25
 
26
26
  Turning sharing off stops future optional collection. It does not stop required processing or delete stored data.
27
27
 
28
+ ## Evaluation connections
29
+
30
+ These features require a matching Scout serving deployment; saving Langfuse
31
+ credentials also requires a dedicated server encryption secret. The npm package
32
+ does not deploy or enable the hosted connector. On enabled deployments, reported
33
+ and imported aggregates are held per authenticated user
34
+ in Cloudflare Durable Objects and inform only that user's recommendations across
35
+ conversations. No cross-customer sharing is enabled. Each aggregate records level,
36
+ kind (quality, USD or seconds), task/scope, catalogue model/endpoint ID, metric,
37
+ average/value, sample count, direction, source label and report/import time.
38
+ Do not put record IDs, emails or sensitive information in labels; identifier-like
39
+ source/scope labels trigger warnings, not guaranteed redaction.
40
+
41
+ Connecting Langfuse sends a separate Viewer key pair and mappings from the
42
+ signed-in browser, headless CLI, authenticated API or trusted MCP client to Scout,
43
+ encrypted at rest with a dedicated Worker secret. Headless management uses a
44
+ separately approved connection-management key; existing recommendation keys do
45
+ not gain that permission. The CLI reads Viewer credentials from a protected stdin
46
+ pipe or secret-manager-injected environment, not command arguments, and does not
47
+ save those Viewer credentials locally. MCP accepts them through client-controlled
48
+ HTTP headers, not model-visible tool arguments. Keep client, proxy and shell logs
49
+ from recording credentials. The management key is stored separately on your
50
+ device until revoked/deleted and can delete all evaluation data for your account;
51
+ disconnecting Langfuse does not revoke it. First-time device authorization still
52
+ requires account sign-in and approval. The web connection UI remains optional.
53
+ Scout requests only experiment headers/metadata and aggregate metric averages
54
+ and counts. It does not request inputs, outputs, trace/observation bodies,
55
+ experiment items, raw scores or score comments. Arbitrary metadata is discarded;
56
+ experiment source labels are hashed. A deliberately invalid write-permission
57
+ probe does not send a valid dataset or create traces. Your Langfuse region does
58
+ not constrain Scout's storage or AI-provider region.
59
+
60
+ At most 5,000 aggregates or 1 MB are retained per user. Rows expire 90 days after
61
+ report/import, are excluded from recommendations at expiry and pruned daily;
62
+ cleanup may be delayed by platform scheduling. The evaluation account, including
63
+ its encrypted connection, is erased after 90 days without a report, connection
64
+ or successful sync. Backups and infrastructure logs follow provider policies;
65
+ immediate backup erasure has not been independently verified.
66
+
67
+ On the signed-in [connection page](/connect/?integration=langfuse), disconnecting
68
+ deletes the key and imported aggregates, including imports from an in-flight
69
+ sync. **Delete all evaluation data** also deletes reported aggregates. Past
70
+ conversation text and recommendations remain and may mention prior results;
71
+ request broader deletion through the contact below. Legacy per-conversation
72
+ aggregates migrate when that conversation next receives a turn. Old conversations
73
+ not reopened require the broader deletion request.
74
+
28
75
  ## Storage and retention
29
76
 
30
77
  - Your browser stores sign-in information, sharing preferences and temporary task drafts.
package/docs/scout.md CHANGED
@@ -33,6 +33,7 @@ OpenMerit does not run evaluations or host an eval platform. Scout tells you wha
33
33
  - **Measured cost.** A result can set `kind` to `usd` or `seconds` (default `quality`): cost or time per model call at the task level, per agent run at the agent level, per outcome at the environment level. Cost and time results are never quality evidence. A model's agent- or task-level `usd` result on a task appears as `cost.measured` (`usdPer1kTasks`, `usdPer1kAgentRuns`, and the `result` it came from), and when every model in a group (measured or unmeasured) has one, the group is ranked by measured cost instead of Scout's catalogue estimate, which can be far off for agent runs.
34
34
  - **Environment results and attribution.** An environment-level result also measures every other agent and control. Scout no longer drops a candidate below the bar when its own task- and agent-level quality results on that task match a candidate that reaches it; an environment miss alone does not show the swapped model caused it.
35
35
  - **Since the Scout serving deploy of 5 November 2026 (version `a4d46878-5c00-4997-b7e6-0d0abd0187e7`) and npm 0.3.4:** models that tie under the conversation's priority, such as equal quality from the same measured results, are ordered by measured cost when both have one, so a quality priority no longer keeps the benchmark order among measured equals. Results stored before `kind` existed count as quality again, so older conversations keep `quality.measured`. `openmerit scout --results` refuses more than 50 results per call before sending them.
36
+ - **Unreleased source (needs a Scout serving deploy):** the per-task workload is per model call, and Scout's instructions now say so, along with what one agent call usually sends and returns; before, the prompt asked for a per-run workload that was then multiplied by the calls per run. When you report measured cost on a task, every fully priced estimate in that task is scaled by the median ratio of measured to estimated cost among the models you measured, shown as `cost.calibration` (`factor` and the `results` it used); the order among estimates does not change. Catalogue prices still exclude prompt caching, so uncalibrated estimates for agents that reuse a cached prompt stay high. When any shortlisted model has measured cost, only measured cost earns the cheapest label (`best: ["cost"]`), so models that share one catalogue route price no longer all receive it.
36
37
  - **Scope.** Results are stored with the conversation for your account and inform only your recommendations. They are not shared with other accounts or used to change Scout's catalogue.
37
38
 
38
39
  The quality bar now considers what an error costs once your environment's controls, such as a person approving every payment, have acted. Cost remains the default ranking among models that reach the bar.
package/docs/security.md CHANGED
@@ -12,6 +12,27 @@ OpenMerit accepts no model-provider or Jev credential. Credentials belong to the
12
12
 
13
13
  Never place secrets in evaluation configuration, task fixtures, Scout project policy, traces, or committed environment files.
14
14
 
15
+ ### Langfuse connector (unreleased)
16
+
17
+ The [connection guide](scout-close-loop.md#connect-langfuse) describes the source
18
+ implementation, not npm 0.3.4 or a deployed acceptance result. Only a separate
19
+ Viewer key pair is accepted. A successful read plus a deliberately invalid
20
+ dataset-create permission probe (HTTP 403 required) fails closed for Full keys;
21
+ no valid write payload is sent. Positive Viewer-key acceptance still needs a real
22
+ Viewer key. Fixed US/EU/HIPAA hosts, manual redirects, timeouts and bounded reads
23
+ prevent credential forwarding and unbounded response bodies. The connector reads
24
+ only experiment headers/metadata and aggregate Metrics v2 values, never trace,
25
+ observation, experiment-item or raw-score bodies. Extra metadata fields are
26
+ discarded; experiment source labels are hashes, not names or record IDs.
27
+
28
+ Credentials and mappings are AES-GCM encrypted in one Cloudflare Durable Object
29
+ per authenticated user, bound to that object's ID with a dedicated Worker secret.
30
+ Keys never enter browser storage, URLs or analytics. Keep that encryption secret
31
+ stable: replacing it invalidates stored connections and requires reconnecting.
32
+ Disconnect deletes the key and imported aggregates; deleting all evaluation data
33
+ also deletes reported aggregates. Neither erases conversation text. Retention,
34
+ limits and processor caveats are in the [data policy](scout-data-policy.md).
35
+
15
36
  ## Local state
16
37
 
17
38
  `.openmerit/` may hold evaluation configuration, task fixtures, reports and budget usage, and Scout project policy, plus files left by the retired Inference Terminal (`terminal.json`, `inference.jsonl`), which upgrades do not delete. It may reveal task names, model choices, budgets or endpoint identifiers. Treat it as sensitive project metadata even though it must not contain credentials.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmerit",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
4
4
  "description": "Evidence-driven model decisions for coding harnesses.",
5
5
  "type": "module",
6
6
  "bin": {