openmerit 0.2.0 → 0.3.1
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 +11 -0
- package/README.md +12 -25
- package/dist/pi/scout-extension.mjs +20 -0
- package/dist/terminal/scout-update.json +1 -1
- package/dist/terminal/src/browser.mjs +2 -4
- package/dist/terminal/src/cli.mjs +4 -56
- package/dist/terminal/src/install.mjs +2 -2
- package/dist/terminal/src/scout-instructions.js +20 -0
- package/dist/terminal/src/scout-pi.js +1 -1
- package/dist/terminal/src/scout.js +35 -9
- package/dist/terminal/src/telemetry.mjs +8 -60
- package/docs/agent-evaluations.md +8 -148
- package/docs/commands.md +3 -9
- package/docs/getting-started.md +2 -16
- package/docs/index.md +4 -18
- package/docs/inference-terminal.md +13 -594
- package/docs/roadmap.md +2 -2
- package/docs/scout-data-policy.md +2 -1
- package/docs/scout.md +21 -2
- package/docs/security.md +5 -18
- package/docs/testing.md +4 -16
- package/docs/troubleshooting.md +2 -2
- package/package.json +10 -23
- package/dist/eval/src/adapters.mjs +0 -112
- package/dist/eval/src/cli.mjs +0 -151
- package/dist/eval/src/config.mjs +0 -108
- package/dist/eval/src/process.mjs +0 -44
- package/dist/eval/src/report.mjs +0 -59
- package/dist/eval/src/runner.mjs +0 -123
- package/dist/eval/src/scout.mjs +0 -48
- package/dist/terminal/public/app.js +0 -298
- package/dist/terminal/public/brands/anthropic.png +0 -0
- package/dist/terminal/public/brands/baai.png +0 -0
- package/dist/terminal/public/brands/baseten.png +0 -0
- package/dist/terminal/public/brands/cerebras.png +0 -0
- package/dist/terminal/public/brands/cohere.png +0 -0
- package/dist/terminal/public/brands/deepseek.ico +0 -0
- package/dist/terminal/public/brands/google.png +0 -0
- package/dist/terminal/public/brands/groq.ico +0 -0
- package/dist/terminal/public/brands/huggingface.svg +0 -8
- package/dist/terminal/public/brands/lm-studio.png +0 -0
- package/dist/terminal/public/brands/meta.ico +0 -0
- package/dist/terminal/public/brands/mistral.png +0 -0
- package/dist/terminal/public/brands/nomic.png +0 -0
- package/dist/terminal/public/brands/ollama.png +0 -0
- package/dist/terminal/public/brands/openai.png +0 -0
- package/dist/terminal/public/brands/openrouter.png +0 -0
- package/dist/terminal/public/brands/qwen.png +0 -0
- package/dist/terminal/public/brands/vllm.png +0 -0
- package/dist/terminal/public/favicon.svg +0 -1
- package/dist/terminal/public/index.html +0 -22
- package/dist/terminal/public/styles.css +0 -921
- package/dist/terminal/src/activity-merge.mjs +0 -66
- package/dist/terminal/src/collect.mjs +0 -370
- package/dist/terminal/src/coverage.mjs +0 -33
- package/dist/terminal/src/discovery.mjs +0 -99
- package/dist/terminal/src/hardware.mjs +0 -57
- package/dist/terminal/src/history.mjs +0 -32
- package/dist/terminal/src/log-continuity.mjs +0 -26
- package/dist/terminal/src/project-activity.mjs +0 -157
- package/dist/terminal/src/repository-background.mjs +0 -37
- package/dist/terminal/src/repository-index.mjs +0 -267
- package/dist/terminal/src/repository-policy.mjs +0 -3
- package/dist/terminal/src/repository-syntax.mjs +0 -236
- package/dist/terminal/src/repository-worker.mjs +0 -15
- package/dist/terminal/src/sample.mjs +0 -171
- package/dist/terminal/src/scout-credentials.d.ts +0 -12
- package/dist/terminal/src/scout-credentials.ts +0 -44
- package/dist/terminal/src/scout-instructions.d.ts +0 -5
- package/dist/terminal/src/scout-instructions.ts +0 -119
- package/dist/terminal/src/scout-pi.d.ts +0 -44
- package/dist/terminal/src/scout-pi.ts +0 -76
- package/dist/terminal/src/scout.d.ts +0 -77
- package/dist/terminal/src/scout.ts +0 -164
- package/dist/terminal/src/server.mjs +0 -95
- package/dist/terminal/src/service-registry.mjs +0 -78
- package/dist/terminal/src/services.mjs +0 -44
- package/dist/terminal/src/topology.mjs +0 -30
- package/dist/terminal/src/updates.d.ts +0 -62
- package/dist/terminal/src/updates.ts +0 -313
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 — 2026-11-05
|
|
4
|
+
|
|
5
|
+
- `openmerit scout --conversation <id> --results <file.json>` reports aggregate results you measured with your own evals. Scout then ranks them above public benchmarks for your recommendations only. Terminal output shows each task's evaluation plan, its agent and calls per run, and marks quality that rests on measured results.
|
|
6
|
+
- The installed Scout instructions for Claude Code and Pi tell the agent to measure shortlisted candidates as extra runs of the project's existing evals, never a second eval, and to report aggregates only. This needs the matching hosted Scout release, whose `scout_models` tool returns `tasks[].evaluation` and accepts `results`.
|
|
7
|
+
|
|
8
|
+
## 0.3.0 — 2026-11-04
|
|
9
|
+
|
|
10
|
+
- **Breaking:** retire the standalone `openmerit eval` CLI, including `init`, `doctor`, `plan`, `run`, `watch` and `report`. Existing local evaluation files are not deleted. The hosted `/scout/api/recommend` retirement ships separately with the Scout serving Worker.
|
|
11
|
+
- **Breaking:** retire the local Inference Terminal. `openmerit dash` and all of its options, including `--demo`, `--json` and `--diagnostics`, are removed, along with repository indexing, request-log and activity-export import, runtime and hardware discovery, the browser interface and its opt-in terminal analytics events. Existing `.openmerit/terminal.json`, `.openmerit/inference.jsonl` and application logs are not deleted. Scout setup, `openmerit scout` and `openmerit update` are unchanged.
|
|
12
|
+
- Drop the terminal-only runtime dependencies (`@iarna/toml`, `@lezer/python`) and browser bundle dependencies.
|
|
13
|
+
|
|
3
14
|
## 0.2.0 — 2026-11-04
|
|
4
15
|
|
|
5
16
|
- Add `openmerit scout`, a conversation with Scout from the terminal: clarifying questions answered with `--brief` or `--skip`, then up to five models per task, cheapest first among models that reach Scout's quality bar. When you ask for the fastest or newest model, the output names that order and shows each model's time per task or release date. It uses this device's Claude Code or Pi key and limits new conversations to the endpoint IDs in `.openmerit/scout.json`.
|
package/README.md
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
1
|
# OpenMerit
|
|
2
2
|
|
|
3
|
-
OpenMerit provides
|
|
3
|
+
OpenMerit provides Scout guidance for application-model choices: in the browser, from the `openmerit scout` command, and through MCP for Claude Code and Pi.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Open the terminal
|
|
8
|
-
|
|
9
|
-
Requires Node.js 22.19 or newer and npm:
|
|
10
|
-
|
|
11
|
-
```sh
|
|
12
|
-
npm install -g openmerit
|
|
13
|
-
cd /path/to/your/application
|
|
14
|
-
openmerit dash
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
No Pi installation or provider API key is needed. The terminal opens a browser connected to a read-only loopback service. Supported source/configuration declarations establish inventory; existing application logs or exports supply request history. Prompts, responses, and credentials are not retained by terminal collection. Unsupported or missing evidence remains unknown.
|
|
18
|
-
|
|
19
|
-
The terminal follows the device's light/dark preference without a theme control. CLI and terminal product analytics are off unless explicitly enabled; see [the analytics contract](docs/inference-terminal.md#optional-cli-product-analytics).
|
|
20
|
-
|
|
21
|
-
Run `openmerit dash --demo` to explore a fictional sample workspace. See [the terminal guide](docs/inference-terminal.md) for configuration, discovery, diagnostics, history limits, and agent access.
|
|
5
|
+
Scout recommends candidates to evaluate. It is not a request router or proof of application-quality improvements.
|
|
22
6
|
|
|
23
7
|
## Connect Scout to Claude Code
|
|
24
8
|
|
|
@@ -38,17 +22,21 @@ There are no `/openmerit` lifecycle commands, evaluation setup, scheduler, Launc
|
|
|
38
22
|
|
|
39
23
|
Package publication and website deployment are separate releases. Pi setup requires OpenMerit 0.1.12 or newer; check the resolved package version when using `@latest`.
|
|
40
24
|
|
|
41
|
-
##
|
|
25
|
+
## Evaluation CLI retirement — npm 0.3.0
|
|
26
|
+
|
|
27
|
+
OpenMerit 0.3.0 removes the standalone `openmerit eval` commands, including planning. Existing local evaluation files are not deleted. The old Scout JSON recommendation API went offline with the Scout serving deploy of 5 November 2026. Scout's conversational agent, MCP, coding-agent setup and catalogue publication remain available; see [retirement details](docs/agent-evaluations.md).
|
|
28
|
+
|
|
29
|
+
## Inference Terminal retirement — npm 0.3.0
|
|
42
30
|
|
|
43
|
-
|
|
31
|
+
OpenMerit 0.3.0 also removes the local Inference Terminal: `openmerit dash`, its `--demo` sample, collectors, repository index and browser interface. Existing `.openmerit/terminal.json`, `.openmerit/inference.jsonl` and application logs are not deleted. Older installed packages keep the command until they are upgraded. See [retirement details](docs/inference-terminal.md).
|
|
44
32
|
|
|
45
33
|
## Documentation
|
|
46
34
|
|
|
47
|
-
The [documentation overview](docs/index.md) links the
|
|
35
|
+
The [documentation overview](docs/index.md) links the maintained Markdown guides. The unpublished `/docs/` website shell is retired; the live Scout data policy and protected memo have separate builds.
|
|
48
36
|
|
|
49
37
|
- [Getting started](docs/getting-started.md) and [troubleshooting](docs/troubleshooting.md)
|
|
50
38
|
- [Scout](docs/scout.md) and [Pi adapter](docs/pi-extension.md)
|
|
51
|
-
- [Inference Terminal](docs/inference-terminal.md) and [agent evaluations](docs/agent-evaluations.md)
|
|
39
|
+
- Retired: [Inference Terminal](docs/inference-terminal.md) and [agent evaluations](docs/agent-evaluations.md)
|
|
52
40
|
- [Security and data handling](docs/security.md)
|
|
53
41
|
- [Testing](docs/testing.md) and [current limitations](docs/roadmap.md)
|
|
54
42
|
|
|
@@ -57,9 +45,8 @@ The [documentation overview](docs/index.md) links the current guides. The websit
|
|
|
57
45
|
```sh
|
|
58
46
|
npm ci
|
|
59
47
|
npm run verify
|
|
60
|
-
npm run dash -- /path/to/application
|
|
61
48
|
```
|
|
62
49
|
|
|
63
|
-
`packages/terminal` contains the CLI,
|
|
50
|
+
`packages/terminal` contains the `openmerit` CLI: Scout setup, updates and the `scout` command; `packages/pi` supplies only the native Scout adapter. The package exports `openmerit/pi` and the `openmerit` command. See the [Pi package README](packages/pi/README.md) for bounded adapter tests. Provider credentials belong to the harness or its environment and must never be committed.
|
|
64
51
|
|
|
65
|
-
Edit documentation in `docs/`, then run `npm run check:docs`.
|
|
52
|
+
Edit documentation in `docs/`, then run `npm run check:docs`. `npm run build:site` builds only the Scout data policy and protected memo, not a docs website. A local build does not publish changes; follow [deployment maintenance](docs/site-deployment.md) before any approved release.
|
|
@@ -117,6 +117,26 @@ Compare quality evidence, reference cost/time and watch-outs. Prefer the cheapes
|
|
|
117
117
|
usable option the evidence supports, without claiming benchmarks prove
|
|
118
118
|
application accuracy. Use the project's actual SDK and protocol.
|
|
119
119
|
|
|
120
|
+
## Measuring candidates
|
|
121
|
+
|
|
122
|
+
Each task also has an \`evaluation\`: the checks (per model call, agent run or
|
|
123
|
+
end-to-end outcome), labelled examples per candidate and when to stop a losing
|
|
124
|
+
candidate. Offer to run it before the user commits to a model. Use the evals the
|
|
125
|
+
project already has, such as datasets and experiments in its eval or tracing
|
|
126
|
+
platform, or its own eval scripts and fixtures: add the shortlisted candidates as
|
|
127
|
+
extra runs of those evals. Do not build a second eval or copy their datasets.
|
|
128
|
+
If the project has no eval for the task, propose a minimal one built from its
|
|
129
|
+
existing traces and the decisions people already make, and create it only where
|
|
130
|
+
and how the user agrees. Ask before spending on model calls or writing to an
|
|
131
|
+
eval platform.
|
|
132
|
+
|
|
133
|
+
Report what you measured with \`scout_models\`: the same \`conversationId\` and
|
|
134
|
+
\`results\`, one aggregate per model and metric (\`level\`, \`task\` title, \`scope\`,
|
|
135
|
+
\`model\` endpoint or model ID, \`metric\`, \`value\`, \`higherIsBetter\`,
|
|
136
|
+
\`samples\`, \`source\`). Send aggregates only, never inputs, outputs or records.
|
|
137
|
+
Scout ranks them above public benchmarks for this user only; models whose
|
|
138
|
+
\`quality.measured\` is true rest on them.
|
|
139
|
+
|
|
120
140
|
## Boundaries
|
|
121
141
|
|
|
122
142
|
Ask before adding providers or credentials, spending, deploying or external
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"format":1,"contract":"scout-recommend-v1","version":"0.
|
|
1
|
+
{"format":1,"contract":"scout-recommend-v1","version":"0.3.1","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`). Send aggregates 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. 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,9 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
const terminalURL = url => { const target = new URL(url); return target.protocol === 'http:' && target.hostname === '127.0.0.1' && /^http:\/\/127\.0\.0\.1:[0-9]+$/.test(url) && !target.username && !target.password && target.pathname === '/' && !target.search && !target.hash; };
|
|
6
|
-
export function browserCommand(url, platform = process.platform, env = process.env, allowed = terminalURL) {
|
|
3
|
+
// Callers decide which URLs may open. Never use a shell.
|
|
4
|
+
export function browserCommand(url, platform = process.platform, env = process.env, allowed) {
|
|
7
5
|
if (!allowed(url)) throw new Error('Refusing to open an unexpected URL.');
|
|
8
6
|
if (env.SSH_CONNECTION || env.SSH_TTY) return null;
|
|
9
7
|
if (platform === 'darwin') return ['open', [url]];
|
|
@@ -1,77 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { stat } from 'node:fs/promises';
|
|
4
|
-
import { startTerminal } from './server.mjs';
|
|
5
|
-
import { createCollector, loadConfig } from './collect.mjs';
|
|
6
|
-
import { sampleSnapshot } from './sample.mjs';
|
|
7
|
-
import { openBrowser } from './browser.mjs';
|
|
8
2
|
import { commandTelemetry } from './telemetry.mjs';
|
|
9
3
|
|
|
10
|
-
const help = `OpenMerit
|
|
4
|
+
const help = `OpenMerit
|
|
11
5
|
|
|
12
|
-
openmerit dash [path] [options]
|
|
13
6
|
openmerit scout --help Ask Scout which models fit your tasks
|
|
14
7
|
openmerit install --help Connect OpenMerit to your coding agent
|
|
15
8
|
openmerit update --help Update installed Scout instructions
|
|
16
|
-
openmerit
|
|
17
|
-
|
|
18
|
-
--port <number> Require a specific local port (default: 4310 or a free port)
|
|
19
|
-
--interval <seconds> Collection interval, 10–30 (default: 15)
|
|
20
|
-
--no-discovery Skip project/log discovery and extra runtime discovery
|
|
21
|
-
--no-open Print the URL without opening a browser
|
|
22
|
-
--json Print one snapshot for an agent, then exit
|
|
23
|
-
--diagnostics Print shareable index coverage counts, then exit
|
|
24
|
-
--demo Explore a clearly labeled fictional environment
|
|
25
|
-
--help Show this help
|
|
26
|
-
|
|
27
|
-
Opens your default browser after the local terminal is ready.
|
|
28
|
-
Reads application metadata and local runtime inventory. Makes no inference calls.
|
|
29
|
-
Coding-harness usage is outside this terminal's scope.
|
|
9
|
+
openmerit --help Show this help
|
|
30
10
|
`;
|
|
31
11
|
|
|
32
12
|
async function main() {
|
|
33
13
|
const args = process.argv.slice(2);
|
|
34
14
|
if (['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor', 'update'].includes(args[0])) { const { main } = await import('./install.mjs'); process.exitCode = await main(args); return; }
|
|
35
15
|
if (args[0] === 'scout') { const { main } = await import('./scout.js'); process.exitCode = await main(args.slice(1)); return; }
|
|
36
|
-
if (args[0] === 'eval') { const { main } = await import('../../eval/src/cli.mjs'); await main(args.slice(1)); return; }
|
|
37
16
|
if (!args.length || args.includes('--help') || args.includes('-h')) { console.log(help); return; }
|
|
38
|
-
|
|
39
|
-
let root = process.cwd(), port = 4310, interval = 15, demo = false, json = false, pathSeen = false, discovery = true, noOpen = false, explicitPort = false, diagnostics = false;
|
|
40
|
-
for (let i = 0; i < args.length; i++) {
|
|
41
|
-
const arg = args[i];
|
|
42
|
-
if (arg === '--port' || arg === '--interval') {
|
|
43
|
-
const value = args[++i];
|
|
44
|
-
if (!/^\d+$/.test(value ?? '')) throw new Error(`${arg} requires an integer.`);
|
|
45
|
-
if (arg === '--port') { port = Number(value); explicitPort = true; } else interval = Number(value);
|
|
46
|
-
} else if (arg === '--no-open') noOpen = true;
|
|
47
|
-
else if (arg === '--no-discovery') discovery = false;
|
|
48
|
-
else if (arg === '--access' || arg === '--revoke-access') throw new Error('Coding-harness history is no longer collected. Saved history permissions are ignored; these access options have been removed.');
|
|
49
|
-
else if (arg === '--demo') demo = true;
|
|
50
|
-
else if (arg === '--json') json = true;
|
|
51
|
-
else if (arg === '--diagnostics') diagnostics = true;
|
|
52
|
-
else if (!arg.startsWith('-') && !pathSeen) { root = resolve(arg); pathSeen = true; }
|
|
53
|
-
else throw new Error(`Unknown argument: ${arg}`);
|
|
54
|
-
}
|
|
55
|
-
if (port < 1 || port > 65535) throw new Error('Port must be between 1 and 65535.');
|
|
56
|
-
if (interval < 10 || interval > 30) throw new Error('Collection interval must be between 10 and 30 seconds.');
|
|
57
|
-
if (!(await stat(root)).isDirectory()) throw new Error('Choose a project directory.');
|
|
58
|
-
const config = demo ? null : await loadConfig(root);
|
|
59
|
-
if (config && !discovery) config.discovery = false;
|
|
60
|
-
if (diagnostics) { const {indexRepository,indexDiagnostics}=await import('./repository-index.mjs'); const index = !demo && config.discovery ? await indexRepository(root) : null; console.log(JSON.stringify(indexDiagnostics(index), null, 2)); return; }
|
|
61
|
-
if (json) { console.log(JSON.stringify(demo ? sampleSnapshot() : await createCollector(root, config, interval)(), null, 2)); return; }
|
|
62
|
-
const terminal = await startTerminal({ root, port, interval, demo, config, fallbackPort: !explicitPort });
|
|
63
|
-
console.log(`\n OpenMerit · Inference Terminal\n\n ${terminal.url}\n\n ${demo ? 'Sample environment · fictional data' : 'Current project · passive collection'}\n Updates every ${interval}s. Press Ctrl+C to close.\n`);
|
|
64
|
-
const close = () => { void terminal.close().then(() => process.exit(0)); };
|
|
65
|
-
process.once('SIGINT', close); process.once('SIGTERM', close);
|
|
66
|
-
if (!demo && config.discovery) {
|
|
67
|
-
console.log(' Automatic discovery: repository code/configuration, saved request logs, and project-linked runtimes.');
|
|
68
|
-
}
|
|
69
|
-
if (!noOpen && !await openBrowser(terminal.url)) console.log(' Open the local URL above in your browser.');
|
|
17
|
+
throw new Error('Unknown command. Run openmerit --help.');
|
|
70
18
|
}
|
|
71
19
|
const completeTelemetry = commandTelemetry(process.argv.slice(2));
|
|
72
20
|
const setupCodes = { 10: 'approval_pending', 11: 'access_denied', 20: 'install_conflict', 30: 'openmerit_unreachable' };
|
|
73
21
|
main().then(() => completeTelemetry(process.exitCode ? 'error' : 'ok', undefined, setupCodes[process.exitCode]), async error => {
|
|
74
|
-
console.error(`\n ${error.
|
|
22
|
+
console.error(`\n ${error.message}\n`);
|
|
75
23
|
process.exitCode = 1;
|
|
76
24
|
await completeTelemetry('error', error);
|
|
77
25
|
});
|
|
@@ -15,13 +15,13 @@ export { rule, skill } from './scout-instructions.js';
|
|
|
15
15
|
// One-command coding-agent setup: device sign-in in the browser, then the MCP
|
|
16
16
|
// server, startup rule and Scout skill. Built to be run by a person or by a coding agent:
|
|
17
17
|
// no interactive prompts, bounded waits, resumable logins and explicit exit codes.
|
|
18
|
-
|
|
18
|
+
const exitCodes = { ok: 0, error: 1, pending: 10, denied: 11, conflict: 20, unreachable: 30 };
|
|
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
22
|
const clients = { 'claude-code': 'Claude Code', pi: 'Pi' };
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const help = `OpenMerit · coding-agent setup
|
|
25
25
|
|
|
26
26
|
openmerit install [--client claude-code|pi] [--scope user|project] [--setup-id UUID] [--auto-update on|off] [--plan] [--json] [--no-open]
|
|
27
27
|
openmerit status [--client claude-code|pi] [--scope user|project] [--check] [--json]
|
|
@@ -83,6 +83,26 @@ Compare quality evidence, reference cost/time and watch-outs. Prefer the cheapes
|
|
|
83
83
|
usable option the evidence supports, without claiming benchmarks prove
|
|
84
84
|
application accuracy. Use the project's actual SDK and protocol.
|
|
85
85
|
|
|
86
|
+
## Measuring candidates
|
|
87
|
+
|
|
88
|
+
Each task also has an \`evaluation\`: the checks (per model call, agent run or
|
|
89
|
+
end-to-end outcome), labelled examples per candidate and when to stop a losing
|
|
90
|
+
candidate. Offer to run it before the user commits to a model. Use the evals the
|
|
91
|
+
project already has, such as datasets and experiments in its eval or tracing
|
|
92
|
+
platform, or its own eval scripts and fixtures: add the shortlisted candidates as
|
|
93
|
+
extra runs of those evals. Do not build a second eval or copy their datasets.
|
|
94
|
+
If the project has no eval for the task, propose a minimal one built from its
|
|
95
|
+
existing traces and the decisions people already make, and create it only where
|
|
96
|
+
and how the user agrees. Ask before spending on model calls or writing to an
|
|
97
|
+
eval platform.
|
|
98
|
+
|
|
99
|
+
Report what you measured with \`scout_models\`: the same \`conversationId\` and
|
|
100
|
+
\`results\`, one aggregate per model and metric (\`level\`, \`task\` title, \`scope\`,
|
|
101
|
+
\`model\` endpoint or model ID, \`metric\`, \`value\`, \`higherIsBetter\`,
|
|
102
|
+
\`samples\`, \`source\`). Send aggregates only, never inputs, outputs or records.
|
|
103
|
+
Scout ranks them above public benchmarks for this user only; models whose
|
|
104
|
+
\`quality.measured\` is true rest on them.
|
|
105
|
+
|
|
86
106
|
## Boundaries
|
|
87
107
|
|
|
88
108
|
Ask before adding providers or credentials, spending, deploying or external
|
|
@@ -26,7 +26,7 @@ export function piPaths(root, scope) {
|
|
|
26
26
|
const base = scope === 'user' ? agentDir : join(root, '.pi');
|
|
27
27
|
return { skill: join(base, 'skills/openmerit-scout/SKILL.md'), rule: join(base, 'extensions/openmerit-scout.ts'),
|
|
28
28
|
connection: join(base, 'extensions/openmerit-scout.json'), record: join(base, 'openmerit-install.json'),
|
|
29
|
-
|
|
29
|
+
mcpFile: join(base, 'mcp.json'), mcpScope: scope, agentDir };
|
|
30
30
|
}
|
|
31
31
|
export async function inspectPi(root, scope) {
|
|
32
32
|
const paths = piPaths(root, scope);
|
|
@@ -3,20 +3,25 @@
|
|
|
3
3
|
// the same scout_models MCP tool with this device's key, so every client gets the
|
|
4
4
|
// same clarifying questions and per-task shortlists.
|
|
5
5
|
import { readFile } from 'node:fs/promises';
|
|
6
|
-
import { join } from 'node:path';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
7
|
import { credentialsPath, readCredentials } from "./scout-credentials.js";
|
|
8
|
-
|
|
8
|
+
const policyFile = '.openmerit/scout.json';
|
|
9
9
|
export const components = ['input', 'work', 'output', 'criteria', 'constraints'];
|
|
10
|
-
|
|
10
|
+
const help = `OpenMerit · Scout
|
|
11
11
|
|
|
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
|
+
openmerit scout --conversation <id> --results <file.json> [--json]
|
|
14
15
|
|
|
15
16
|
Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
|
|
16
17
|
brief with clarifying questions: answer them with --brief (components: input,
|
|
17
18
|
work, output, criteria, constraints) in the same conversation, or --skip to let
|
|
18
19
|
Scout assume. It then splits the work into tasks and recommends up to five
|
|
19
|
-
models for each
|
|
20
|
+
models for each, with an evaluation: how to measure the candidates with your
|
|
21
|
+
own evals. Report what you measured with --results: a JSON list of aggregate
|
|
22
|
+
results ({level, task?, scope, model, metric, value, higherIsBetter, samples,
|
|
23
|
+
source}), never inputs or outputs. Scout ranks them above public benchmarks, for
|
|
24
|
+
your recommendations only. A turn usually takes one to three minutes.
|
|
20
25
|
|
|
21
26
|
A new conversation is limited to the endpoint IDs in ${policyFile}
|
|
22
27
|
(set with openmerit project). --all-endpoints searches the whole catalogue and is
|
|
@@ -46,7 +51,7 @@ function deviceKey(root) {
|
|
|
46
51
|
return null;
|
|
47
52
|
}
|
|
48
53
|
// One turn through the scout_models MCP tool. Errors are written for the person or agent running it.
|
|
49
|
-
|
|
54
|
+
async function askScout(root, request, signal) {
|
|
50
55
|
const credentials = deviceKey(root);
|
|
51
56
|
if (!credentials)
|
|
52
57
|
throw new Error('This device is not signed in to OpenMerit. Run: npx -y openmerit@latest install');
|
|
@@ -114,7 +119,7 @@ export function renderScout(result) {
|
|
|
114
119
|
const tasks = result.tasks ?? [];
|
|
115
120
|
tasks.forEach((task, i) => {
|
|
116
121
|
if (tasks.length > 1)
|
|
117
|
-
lines.push(`${i + 1}. ${task.title}`, ` ${task.work}`);
|
|
122
|
+
lines.push(`${i + 1}. ${task.title}${task.agent ? ` (${task.agent}${task.callsPerRun && task.callsPerRun > 1 ? `, ${task.callsPerRun} calls per run` : ''})` : ''}`, ` ${task.work}`);
|
|
118
123
|
// As on the page, the default cheapest-first order is unlabelled; another priority is named with the value it sorts by.
|
|
119
124
|
const priority = task.priority ?? 'cost';
|
|
120
125
|
if (priority !== 'cost')
|
|
@@ -123,14 +128,20 @@ export function renderScout(result) {
|
|
|
123
128
|
const price = m.cost ? `${money(m.cost.inputPerMillion)} in / ${money(m.cost.outputPerMillion)} out per 1M via ${m.cost.route} (${m.cost.endpointId})` : 'no priced route';
|
|
124
129
|
const metric = priority === 'latency' ? ` · ${m.latency ? `${m.latency.seconds.toFixed(1)}s per task` : 'time unknown'}`
|
|
125
130
|
: priority === 'recency' ? ` · ${m.recency ? `released ${m.recency.released}` : 'release date unknown'}` : '';
|
|
126
|
-
lines.push(` ${m.rank}. ${m.name}${metric} · quality ${m.quality.score}/100 · ${price}`, ` ${m.rationale}`);
|
|
131
|
+
lines.push(` ${m.rank}. ${m.name}${metric} · quality ${m.quality.score}/100${m.quality.measured ? ' (measured)' : ''} · ${price}`, ` ${m.rationale}`);
|
|
127
132
|
}
|
|
128
|
-
lines.push(` ${task.summary}
|
|
133
|
+
lines.push(` ${task.summary}`);
|
|
134
|
+
if (task.evaluation)
|
|
135
|
+
lines.push(` Measure with your evals (${task.evaluation.samples} labelled examples per candidate; ${task.evaluation.stopEarly}):`, ...task.evaluation.checks.map(c => ` - ${c.level}: ${c.metric}. ${c.how}`));
|
|
136
|
+
lines.push('');
|
|
129
137
|
});
|
|
130
138
|
if (result.assumptions?.length)
|
|
131
139
|
lines.push('Assumptions:', ...result.assumptions.map(a => ` - ${a}`), '');
|
|
132
140
|
lines.push(`Refine: openmerit scout --conversation ${result.conversationId} "<follow-up>"`);
|
|
133
|
-
lines.push(
|
|
141
|
+
lines.push(`Report measured results: openmerit scout --conversation ${result.conversationId} --results <file.json>`);
|
|
142
|
+
lines.push(tasks.some(t => t.models.some(m => m.quality.measured))
|
|
143
|
+
? 'Quality marked (measured) rests on the results you reported; the rest is Scout\'s estimate from public benchmarks.'
|
|
144
|
+
: 'Quality is Scout\'s estimate from public benchmarks, not measured accuracy for your application.');
|
|
134
145
|
}
|
|
135
146
|
else if (result.status === 'working')
|
|
136
147
|
lines.push(`Scout is still working. Read the outcome with: openmerit scout --conversation ${result.conversationId}`);
|
|
@@ -164,6 +175,19 @@ export async function main(args, { root = process.cwd(), print = console.log } =
|
|
|
164
175
|
request.allEndpoints = true;
|
|
165
176
|
else if (arg === '--json')
|
|
166
177
|
json = true;
|
|
178
|
+
else if (arg === '--results') {
|
|
179
|
+
const file = value();
|
|
180
|
+
let results;
|
|
181
|
+
try {
|
|
182
|
+
results = JSON.parse(await readFile(resolve(root, file), 'utf8'));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new Error(`Could not read results from ${file}: it must be a JSON list.`);
|
|
186
|
+
}
|
|
187
|
+
if (!Array.isArray(results))
|
|
188
|
+
throw new Error(`${file} must hold a JSON list of results.`);
|
|
189
|
+
request.results = results;
|
|
190
|
+
}
|
|
167
191
|
else if (arg.startsWith('-'))
|
|
168
192
|
throw new Error(`Unknown option for scout: ${arg}`);
|
|
169
193
|
else
|
|
@@ -175,6 +199,8 @@ export async function main(args, { root = process.cwd(), print = console.log } =
|
|
|
175
199
|
print(help);
|
|
176
200
|
return 1;
|
|
177
201
|
}
|
|
202
|
+
if (request.results && !request.conversationId)
|
|
203
|
+
throw new Error('--results continues a conversation: pass --conversation <id> from the recommendation you measured.');
|
|
178
204
|
const result = await scoutTurn(root, request);
|
|
179
205
|
print(json ? JSON.stringify(result, null, 2) : renderScout(result));
|
|
180
206
|
return result.status === 'error' ? 1 : 0;
|
|
@@ -5,12 +5,8 @@ export let clientVersion;
|
|
|
5
5
|
for (const path of ['../../../package.json', '../../../../package.json']) {
|
|
6
6
|
try { const pkg = JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')); if (pkg.name === 'openmerit') { clientVersion = pkg.version; break; } } catch {}
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
const applicationURLs = frameFiles.
|
|
10
|
-
const diagnostics = new WeakMap();
|
|
11
|
-
export function scoutDiagnostic(error, trace, code) {
|
|
12
|
-
if (error && typeof error === 'object' && ['scout_network_error', 'scout_http_error', 'scout_response_error', 'scout_timeout'].includes(code)) diagnostics.set(error, { operation: 'scout_recommend', code, ...(typeof trace === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(trace) ? { trace_id: trace } : {}) });
|
|
13
|
-
}
|
|
8
|
+
const frameFiles = Object.freeze(['cli.mjs', 'install.mjs']);
|
|
9
|
+
const applicationURLs = frameFiles.map(file => new URL(`../../terminal/src/${file}`, import.meta.url).href);
|
|
14
10
|
export function safeFrames(error) {
|
|
15
11
|
try {
|
|
16
12
|
const stack = error?.stack;
|
|
@@ -24,7 +20,7 @@ export function safeFrames(error) {
|
|
|
24
20
|
} catch { return []; }
|
|
25
21
|
}
|
|
26
22
|
export function errorProperties(error) {
|
|
27
|
-
try { return { error_type: errorType(error), frames: safeFrames(error)
|
|
23
|
+
try { return { error_type: errorType(error), frames: safeFrames(error) }; }
|
|
28
24
|
catch { return { error_type: 'Error', frames: [] }; }
|
|
29
25
|
}
|
|
30
26
|
const environment = () => ({ ...(clientVersion ? { client_version: clientVersion } : {}), platform: ['darwin', 'linux', 'win32'].includes(process.platform) ? process.platform : 'unknown', runtime_major: Number(process.versions.node.split('.')[0]) });
|
|
@@ -99,65 +95,17 @@ export async function sendInstallationEvent(input) {
|
|
|
99
95
|
} catch { /* Never affect setup, even if the transport ignores abort. */ }
|
|
100
96
|
finally { clearTimeout(timer); }
|
|
101
97
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const out = {};
|
|
106
|
-
const select = (key, values) => { if (!values.includes(input[key])) return false; out[key] = input[key]; return true; };
|
|
107
|
-
if (event === 'terminal_error') {
|
|
108
|
-
if (!select('code', ['collection_error', 'snapshot_error', 'render_error', 'uncaught_error']) || !select('error_type', ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError', 'URIError', 'EvalError', 'AggregateError', 'AbortError'])) return null;
|
|
109
|
-
if (Array.isArray(input.frames)) out.frames = input.frames.slice(0, 8).filter(f => f && frameFiles.includes(f.filename) && Number.isInteger(f.lineno) && f.lineno > 0 && f.lineno <= 1000000 && Number.isInteger(f.colno) && f.colno > 0 && f.colno <= 1000000).map(f => ({ filename: f.filename, lineno: f.lineno, colno: f.colno }));
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
if (!select('mode', ['sample', 'live'])) return null;
|
|
113
|
-
if (event === 'terminal_visited') return out;
|
|
114
|
-
if (event === 'terminal_page_viewed') return select('page', ['overview', 'models', 'services', 'activity', 'compute', 'sources', 'connections']) ? out : null;
|
|
115
|
-
if (event === 'terminal_interaction') return select('action', ['inspect', 'filter', 'search', 'map_expand', 'mode', 'command']) && select('kind', ['model', 'agent', 'task', 'service', 'source', 'host', 'deployment', 'pool', 'record', 'path', 'other']) ? out : null;
|
|
116
|
-
if (event !== 'terminal_snapshot') return null;
|
|
117
|
-
for (const key of ['model_count', 'record_count', 'source_count', 'host_count', 'deployment_count', 'agent_count']) {
|
|
118
|
-
if (!Number.isInteger(input[key]) || input[key] < 0 || input[key] > 1000000000) return null;
|
|
119
|
-
out[key] = input[key];
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
// Shared, process-wide budget; no queue and at most four in-flight requests.
|
|
124
|
-
let windowStart = 0, count = 0, active = 0;
|
|
125
|
-
export function sendTerminalEvent(event, input) {
|
|
126
|
-
if (!telemetryEnabled()) return;
|
|
127
|
-
const properties = terminalProperties(event, input);
|
|
128
|
-
if (!properties) return;
|
|
129
|
-
if (Date.now() - windowStart >= 60000) { windowStart = Date.now(); count = 0; }
|
|
130
|
-
if (count >= 60 || active >= 4) return;
|
|
131
|
-
count++; active++;
|
|
132
|
-
void sendAnalytics(event, properties).finally(() => active--);
|
|
133
|
-
}
|
|
134
|
-
async function sendAnalytics(event, properties) {
|
|
135
|
-
const controller = new AbortController();
|
|
136
|
-
const body = JSON.stringify({ event, event_id: randomUUID(), anonymousId, properties: { ...properties, ...environment() } });
|
|
137
|
-
let timer;
|
|
138
|
-
try {
|
|
139
|
-
await Promise.race([
|
|
140
|
-
Promise.resolve().then(() => fetch('https://openmerit.ai/scout/api/analytics', {
|
|
141
|
-
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Scout-Client': 'cli', 'X-Scout-Analytics': 'on', ...(process.env.OPENMERIT_ANALYTICS_ACCOUNT === 'off' ? { 'X-Scout-Analytics-Account': 'off' } : {}) },
|
|
142
|
-
body, signal: controller.signal, redirect: 'error',
|
|
143
|
-
})).then(response => response.body?.cancel()),
|
|
144
|
-
new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(); }, 750); timer.unref(); }),
|
|
145
|
-
]);
|
|
146
|
-
} catch { /* Never affect product behavior. */ }
|
|
147
|
-
finally { clearTimeout(timer); }
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Consent is independent of terminal discovery. Command identity is ephemeral;
|
|
151
|
-
// the installer separately manages the owned installation's correlation IDs.
|
|
98
|
+
const errorType = error => ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError', 'URIError', 'EvalError', 'AggregateError', 'AbortError'].includes(error?.name) ? error.name : 'Error';
|
|
99
|
+
// Command identity is ephemeral; the installer separately manages the owned
|
|
100
|
+
// installation's correlation IDs.
|
|
152
101
|
export function commandTelemetry(args) {
|
|
153
102
|
const setup = ['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor'];
|
|
154
103
|
const enabled = () => telemetryEnabled(setup.includes(args[0]));
|
|
155
104
|
if (!enabled()) return async () => {};
|
|
156
105
|
const started = performance.now();
|
|
157
106
|
const command_id = randomUUID();
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
? 'help' : args[0] === 'dash' ? 'dash' : setup.includes(args[0]) ? args[0] : args[0] === 'eval' && actions.includes(args[1]) ? `eval_${args[1]}` : 'unknown';
|
|
107
|
+
const command = !args.length || args.includes('--help') || args.includes('-h')
|
|
108
|
+
? 'help' : setup.includes(args[0]) ? args[0] : 'unknown';
|
|
161
109
|
async function send(event, properties) {
|
|
162
110
|
if (!enabled()) return;
|
|
163
111
|
const controller = new AbortController();
|