openmerit 0.3.11 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +74 -22
  2. package/data/eci-estimates.csv +179 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +180 -0
  5. package/dist/connectors/langfuse/client.d.ts +42 -0
  6. package/dist/connectors/langfuse/client.js +141 -0
  7. package/dist/connectors/langfuse/index.d.ts +163 -0
  8. package/dist/connectors/langfuse/index.js +378 -0
  9. package/dist/connectors/langfuse/otlp.d.ts +60 -0
  10. package/dist/connectors/langfuse/otlp.js +40 -0
  11. package/dist/index.d.ts +11 -0
  12. package/dist/index.js +11 -0
  13. package/dist/pi/extension.d.ts +13 -0
  14. package/dist/pi/extension.js +148 -0
  15. package/dist/pi/harness.d.ts +20 -0
  16. package/dist/pi/harness.js +49 -0
  17. package/dist/pi/instructions.d.ts +2 -0
  18. package/dist/pi/instructions.js +12 -0
  19. package/dist/scout/candidates.d.ts +62 -0
  20. package/dist/scout/candidates.js +90 -0
  21. package/dist/scout/eci.d.ts +42 -0
  22. package/dist/scout/eci.js +84 -0
  23. package/dist/scout/evaluators/module.d.ts +24 -0
  24. package/dist/scout/evaluators/module.js +19 -0
  25. package/dist/scout/evaluators/pi.d.ts +53 -0
  26. package/dist/scout/evaluators/pi.js +117 -0
  27. package/dist/scout/pipeline.d.ts +111 -0
  28. package/dist/scout/pipeline.js +157 -0
  29. package/dist/scout/report.d.ts +3 -0
  30. package/dist/scout/report.js +42 -0
  31. package/dist/scout/search.d.ts +110 -0
  32. package/dist/scout/search.js +205 -0
  33. package/dist/scout/service.d.ts +148 -0
  34. package/dist/scout/service.js +143 -0
  35. package/examples/health-declaration-codes/dataset.jsonl +14 -0
  36. package/examples/sahra-enrollment/evaluator.ts +151 -0
  37. package/package.json +34 -78
  38. package/CHANGELOG.md +0 -255
  39. package/dist/pi/scout-extension.mjs +0 -287
  40. package/dist/pi/src/index.d.ts +0 -6
  41. package/dist/pi/src/index.js +0 -56
  42. package/dist/terminal/scout-update.json +0 -1
  43. package/dist/terminal/src/browser.d.mts +0 -9
  44. package/dist/terminal/src/browser.mjs +0 -28
  45. package/dist/terminal/src/cli.mjs +0 -28
  46. package/dist/terminal/src/connections.js +0 -151
  47. package/dist/terminal/src/install.d.mts +0 -2
  48. package/dist/terminal/src/install.mjs +0 -524
  49. package/dist/terminal/src/scout-claude.js +0 -25
  50. package/dist/terminal/src/scout-credentials.js +0 -41
  51. package/dist/terminal/src/scout-instructions.js +0 -213
  52. package/dist/terminal/src/scout-pi.js +0 -84
  53. package/dist/terminal/src/scout.js +0 -229
  54. package/dist/terminal/src/telemetry.d.mts +0 -30
  55. package/dist/terminal/src/telemetry.mjs +0 -138
  56. package/dist/terminal/src/updates.js +0 -397
  57. package/docs/agent-evaluations.md +0 -15
  58. package/docs/commands.md +0 -9
  59. package/docs/getting-started.md +0 -37
  60. package/docs/index.md +0 -25
  61. package/docs/inference-terminal.md +0 -25
  62. package/docs/pi-extension.md +0 -58
  63. package/docs/roadmap.md +0 -16
  64. package/docs/scout-close-loop.md +0 -371
  65. package/docs/scout-data-policy.md +0 -87
  66. package/docs/scout.md +0 -237
  67. package/docs/security.md +0 -107
  68. package/docs/testing.md +0 -114
  69. package/docs/troubleshooting.md +0 -50
@@ -1,287 +0,0 @@
1
- // packages/pi/src/index.ts
2
- import { readFileSync as readFileSync2, statSync } from "node:fs";
3
-
4
- // packages/terminal/src/scout-credentials.ts
5
- import { createHash } from "node:crypto";
6
- import { readFileSync } from "node:fs";
7
- import { homedir } from "node:os";
8
- import { dirname, join, resolve } from "node:path";
9
- function credentialsPath(client, scope, root) {
10
- const base = resolve(process.env.OPENMERIT_CONFIG_DIR ?? join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "openmerit"));
11
- if (client === "claude-code" && scope === "user") return join(base, "credentials.json");
12
- const installation = scope === "project" ? createHash("sha256").update(resolve(root)).digest("hex").slice(0, 24) : "user";
13
- return join(base, client, installation, "credentials.json");
14
- }
15
- function readCredentials(path) {
16
- let value;
17
- try {
18
- value = JSON.parse(readFileSync(path, "utf8"));
19
- } catch (error) {
20
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
21
- throw new Error("Scout credentials could not be read. Run the targeted install command again.");
22
- }
23
- if (!value || typeof value !== "object" || !("version" in value) || value.version !== 1 || !("server" in value) || typeof value.server !== "string" || !("key" in value) || typeof value.key !== "string" || !/^om_[A-Za-z0-9_-]{43}$/.test(value.key)) return null;
24
- const url = new URL(value.server);
25
- if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(url.hostname))) return null;
26
- const result = { version: 1, server: value.server.replace(/\/+$/, ""), key: value.key };
27
- if ("keyId" in value && typeof value.keyId === "string") result.keyId = value.keyId;
28
- if ("headers" in value && value.headers && typeof value.headers === "object") {
29
- result.headers = Object.fromEntries(Object.entries(value.headers).filter(([key, v]) => /^X-Scout-[A-Za-z-]+$/.test(key) && typeof v === "string"));
30
- }
31
- return result;
32
- }
33
-
34
- // packages/terminal/src/scout-instructions.ts
35
- var rule = `# Application model decisions \xB7 OpenMerit Scout
36
-
37
- Before choosing, recommending, adding, downloading, replacing or configuring
38
- ANY application inference model, load the \`openmerit-scout\` skill and consult
39
- \`scout_models\` on the \`openmerit\` MCP server before committing to a model.
40
- This applies even when the user only asks you to build a feature, never mentions
41
- models or Scout, or names a model themselves (assess it; do not silently replace it).
42
-
43
- A pretrained model, local weights, browser inference, an SDK's default model,
44
- embeddings, OCR, speech, image classification, detection and segmentation all
45
- count as model choices, not implementation details. Identify this decision before
46
- writing model-specific code, installing a model package or downloading weights.
47
-
48
- Follow the skill's project access policy. If it is missing, ask and save it first.
49
- Scout may answer with clarifying questions: relay them to the user and continue
50
- the same Scout conversation with their answers before choosing.
51
- If Scout is unavailable, fails or has no suitable evidence/candidate, disclose the
52
- gap and pause the model decision; do not fall back to a remembered or SDK-default
53
- model. You may continue work that does not commit to a model.
54
-
55
- Do not consult Scout for unrelated edits, normal inference calls using an unchanged
56
- approved model, or this coding agent's own model. Do not reselect approved models
57
- unless the task introduces or changes a model decision.
58
-
59
- When the user says "update openmerit", run
60
- \`npx -y openmerit@latest update --scope user\` (use \`--scope project\` if this
61
- rule is in the project's .claude/rules directory). Report the result, including
62
- any conflict or manual-upgrade requirement. Do not delete edited files, bypass
63
- permissions or rerun login automatically. A new session loads updated instructions.
64
- `;
65
- var skill = `---
66
- name: openmerit-scout
67
- description: 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.
68
- ---
69
-
70
- # OpenMerit \xB7 Scout
71
-
72
- Before introducing, recommending, replacing or configuring an application
73
- inference model, call the \`scout_models\` tool on the \`openmerit\` MCP server.
74
- This applies even when the user never mentions models or Scout. Downloading local
75
- weights, using a pretrained browser model or accepting an SDK default is a model
76
- decision: consult Scout before model-specific code, installs or downloads.
77
- Preserve the user's explicit model choice; assess it rather than silently replacing
78
- it. Leave unchanged, approved application selections in place.
79
-
80
- ## Project policy
81
-
82
- Read \`.openmerit/scout.json\` before searching. If it is absent, ask once whether this
83
- project uses **existing-only** (only endpoints it can already reach) or
84
- **existing-first** (prefer them, ask before searching the whole catalogue), and
85
- which public catalogue endpoint IDs it can access. Save the answer:
86
-
87
- \`\`\`sh
88
- npx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...
89
- \`\`\`
90
-
91
- Never infer access from a model name, SDK or environment-variable name. Never
92
- record credentials, private URLs, prompts or customer data. An empty inventory
93
- is not permission to search all models.
94
-
95
- ## Searching
96
-
97
- Call \`scout_models\` with a non-confidential task description as \`prompt\` and
98
- \`endpointIds\` set to the saved inventory, including \`[]\` when empty. Send
99
- only task requirements, never source files, secrets or real customer records.
100
- If the MCP tool is unavailable, run
101
- \`npx -y openmerit@latest scout "<task>" --json\`; it applies the saved inventory.
102
-
103
- Scout is a conversation with the agent behind openmerit.ai/scout. When
104
- \`status\` is \`needs_answers\`, fill in what the user or project already states,
105
- show the user Scout's brief and remaining questions, and wait for their answers.
106
- Then call again with the returned \`conversationId\` and \`brief\` set to the
107
- rewritten components, or \`skip: true\` if the user wants Scout to assume. Never
108
- answer Scout's questions with guesses. Send later refinements to the same
109
- \`conversationId\`. A call with only a \`conversationId\` reads an interrupted turn.
110
-
111
- When \`status\` is \`recommended\`, Scout has split the work into \`tasks\`, one
112
- per model, each with up to five ranked \`models\`; cover every task. Each
113
- model's \`cost\` names the \`endpointId\` and \`providerModelId\` to call.
114
- Existing-only never broadens. For existing-first, if no existing candidate fits a
115
- task, explain the gap and ask before a new conversation without \`endpointIds\`.
116
- Compare quality evidence, reference cost/time and watch-outs. Prefer the cheapest
117
- usable option the evidence supports, without claiming benchmarks prove
118
- application accuracy. Use the project's actual SDK and protocol.
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. First assess the
125
- project's readiness for this task:
126
-
127
- - **No eval setup:** propose a customer-owned setup. Langfuse is the default
128
- suggestion for its supported aggregate connector, not a requirement: existing
129
- local/custom eval scripts can report aggregates without a hosted platform.
130
- Phoenix/Arize and Braintrust are alternatives, not supported Scout connectors.
131
- Ask which system the user wants before creating accounts or resources.
132
- - **Setup exists but evidence is insufficient:** keep that system. Write a
133
- task-specific eval plan in the project's local Markdown decision record for
134
- the user or their coding agent to implement. Name exact candidates/endpoints,
135
- quality floor and task/agent/outcome checks, labelled boundary cases and
136
- controls, sample size, early-stop rule, spend cap, aggregate mappings and
137
- temporary run retention. Explain what the current evidence cannot decide.
138
- - **Suitable evals exist:** reuse their fixtures and definitions; add only the
139
- approved candidate runs. Do not build a second eval or copy their datasets.
140
-
141
- OpenMerit does not host or run these evals. The customer or their coding agent
142
- implements and maintains them with the customer's tools; Scout keeps Viewer-only
143
- access. Ask before model spend or any platform write. Without measured evidence,
144
- Scout may suggest candidates, but do not claim validated model quality.
145
-
146
- Report what you measured with \`scout_models\`: the same \`conversationId\` and
147
- \`results\`, one aggregate per model and metric (\`level\`, \`task\` title, \`scope\`,
148
- \`model\` endpoint or model ID, \`metric\`, \`value\`, \`higherIsBetter\`,
149
- \`samples\`, \`source\`). Set \`kind\` to \`usd\` or \`seconds\` for measured cost
150
- or time (per call at the task level, per run at the agent level), so Scout uses
151
- your cost instead of its estimate. Report each agent's own checks at the agent
152
- level: an end-to-end result also measures the agents you did not swap. Send
153
- aggregates only, never inputs, outputs or records.
154
- Scout ranks them above public benchmarks for this user only; models whose
155
- \`quality.measured\` is true rest on them.
156
-
157
- Swap one agent at a time, holding the other agents, prompts, fixtures and controls
158
- fixed. Use each agent's own checks to attribute failures. If all candidates score
159
- 100%, the eval cannot separate their quality: add harder labelled cases before
160
- claiming equivalent quality. Cost may break the observed tie, not prove equality.
161
-
162
- ## Preserve evidence before cleanup
163
-
164
- Before removing temporary comparison runs, save a local Markdown decision record
165
- with the package/service versions, date, task and policy, exact candidates,
166
- unchanged controls, metric definitions/directions, sample counts, aggregate
167
- quality/cost/time, failures and exclusions, chosen model, limitations and next
168
- review. Record regret against the cheapest measured candidate meeting the same
169
- quality floor; write "not measurable" when comparable evidence is missing.
170
- Exclude secrets, private record IDs, prompts, outputs and trace bodies.
171
-
172
- Temporary comparison traces/records are not permanent: after the record is saved
173
- and checked, propose the exact newly created comparison runs to remove. Obtain
174
- explicit scoped approval, including any cascading deletion, before the customer
175
- or their coding agent deletes them with customer-controlled write access. Scout's
176
- Viewer key cannot do this. Never delete pre-existing customer records, reusable
177
- fixtures, dataset definitions or eval definitions. Verify the approved cleanup
178
- and retained controls, then record the outcome locally. If not approved, leave
179
- records unchanged and state cleanup is pending; do not imply it happened.
180
-
181
- If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
182
- optional. Headless setup requires a separate connection-manager approval with
183
- \`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
184
- expiry; after approval, rerun that command. Existing recommendation keys cannot
185
- manage connections. The user creates a separate Viewer key; receive it through
186
- protected stdin or a secret-manager-injected environment, never chat, tool
187
- arguments, shell literals, traced commands or tracked files. The CLI accepts
188
- non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
189
- and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
190
- A separate trusted MCP connection with the management key exposes
191
- \`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
192
- must come from client-controlled HTTP headers outside model context; if the
193
- client cannot do this securely, use protected CLI input or the web UI instead.
194
- Once the user has confirmed the connection and score/model mappings (including
195
- a connection already recorded in the project), call \`scout_models\` with
196
- \`sync: "langfuse"\` on the first consultation and before a final model decision.
197
- The CLI equivalent is \`openmerit scout --conversation <id> --sync langfuse\`.
198
- Refreshing that approved connection reads aggregates and updates Scout's cache;
199
- it does not run candidates or create/change records in Langfuse. Do not ask for
200
- new spend or platform-write approval just to refresh it. Do not reconstruct and
201
- re-report old measurements from project notes instead of using the connection.
202
- If connection status is unknown, check \`openmerit connect langfuse --status\`
203
- before proposing setup; application tracing keys alone do not prove a Scout
204
- connection. If sync fails, disclose the failure and any use of cached evidence;
205
- do not describe it as fresh. Never read trace bodies to work around missing
206
- mappings or untagged runs; use the aggregate report path for those gaps instead.
207
- Results are stored per user, used in new conversations and
208
- expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
209
- Delete all evaluation data also deletes manually reported aggregates. Obtain
210
- consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
211
- Deleting the connection does not revoke the separate management device key.
212
- Past conversation text is not erased. Never put record IDs, emails or secrets in
213
- \`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
214
-
215
- ## Boundaries
216
-
217
- Ask before adding providers or credentials, spending, deploying or external
218
- writes. Report recommendation, endpoint connectivity and application checks as
219
- separate results, each only after it succeeds. Do not switch this coding agent's
220
- own model.
221
-
222
- If Scout fails or has no suitable candidate/evidence for the required deployment,
223
- say so and pause the model decision; do not substitute a remembered recommendation
224
- or SDK default. Continue only model-independent work. Local/browser models are not
225
- exempt when the catalogue lacks coverage. When an install or login waits for approval (exit code
226
- 10), show the user the approval link and its UTC expiry in your reply, not only in
227
- command output, then rerun the same command once they approve. If it reports that the device key is missing,
228
- expired or revoked, run \`npx -y openmerit@latest login\` and then
229
- \`npx -y openmerit@latest install\` again. Never ask the user to paste a key.
230
- `;
231
- function scoutRule(client) {
232
- if (client === "claude-code") return rule;
233
- return rule.replace("load the `openmerit-scout` skill and consult", "follow the Scout project policy below and consult").replaceAll("`scout_models`", "`mcp__openmerit__scout_models`").replace("Follow the skill's project access policy.", "Follow the project access policy below.").replace("update --scope user", "install --client pi --scope user").replace("rule is in the project's .claude/rules directory", "Pi installation is project-scoped");
234
- }
235
- function scoutSkill(client) {
236
- if (client === "claude-code") return skill;
237
- return skill.replaceAll("`scout_models`", "`mcp__openmerit__scout_models`").replace("openmerit@latest login", "openmerit@latest login --client pi").replace("openmerit@latest install`", "openmerit@latest install --client pi`").replace("again. Never ask the user to paste a key.", "again, using `--scope project` on both commands for a project-scoped installation. Never ask the user to paste a key.");
238
- }
239
-
240
- // packages/pi/src/index.ts
241
- function configuredCredentialsPath() {
242
- const config = new URL("./openmerit-scout.json", import.meta.url);
243
- let value;
244
- try {
245
- if (statSync(config).size > 16384) throw new Error("Invalid Scout connection config");
246
- value = JSON.parse(readFileSync2(config, "utf8"));
247
- } catch (error) {
248
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
249
- return credentialsPath("pi", "user", process.cwd());
250
- }
251
- throw new Error("Invalid Scout connection config");
252
- }
253
- if (!value || typeof value !== "object" || !("credentialsPath" in value) || typeof value.credentialsPath !== "string" || !value.credentialsPath.trim()) {
254
- throw new Error("Invalid Scout connection config");
255
- }
256
- return value.credentialsPath;
257
- }
258
- function createScoutPiExtension(options = {}) {
259
- return (pi) => {
260
- pi.on("session_start", (_event, ctx) => {
261
- try {
262
- const credentials = readCredentials(options.credentialsPath ?? configuredCredentialsPath());
263
- if (!credentials) throw new Error("Scout credentials missing");
264
- pi.registerMcpServer("openmerit", {
265
- type: "http",
266
- url: `${credentials.server}/scout/mcp`,
267
- headers: { ...credentials.headers, Authorization: `Bearer ${credentials.key}` },
268
- exposure: "direct"
269
- });
270
- } catch {
271
- ctx.ui.notify(
272
- "OpenMerit Scout could not load its connection. Run `npx -y openmerit@latest install --client pi --scope user` (use `--scope project` for a project installation), then start a new Pi session. SDK hosts must also load Pi's createMcpExtension().",
273
- "warning"
274
- );
275
- }
276
- });
277
- pi.on("before_agent_start", (event) => {
278
- event.systemPromptOptions.sections.openmerit_scout_rule = scoutRule("pi");
279
- event.systemPromptOptions.sections.openmerit_scout_skill = scoutSkill("pi");
280
- });
281
- };
282
- }
283
- var index_default = createScoutPiExtension();
284
- export {
285
- createScoutPiExtension,
286
- index_default as default
287
- };
@@ -1,6 +0,0 @@
1
- import type { ExtensionFactory } from "@earendil-works/pi-coding-agent";
2
- export declare function createScoutPiExtension(options?: {
3
- credentialsPath?: string;
4
- }): ExtensionFactory;
5
- declare const _default: ExtensionFactory;
6
- export default _default;
@@ -1,56 +0,0 @@
1
- import { readFileSync, statSync } from "node:fs";
2
- import { credentialsPath, readCredentials } from "../../terminal/src/scout-credentials.js";
3
- import { scoutRule, scoutSkill } from "../../terminal/src/scout-instructions.js";
4
- // The installed bundle lives beside this tiny, installer-owned connection file.
5
- // Resolve it at session startup, not at import time or on every model turn.
6
- function configuredCredentialsPath() {
7
- const config = new URL("./openmerit-scout.json", import.meta.url);
8
- let value;
9
- try {
10
- if (statSync(config).size > 16_384)
11
- throw new Error("Invalid Scout connection config");
12
- value = JSON.parse(readFileSync(config, "utf8"));
13
- }
14
- catch (error) {
15
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
16
- return credentialsPath("pi", "user", process.cwd());
17
- }
18
- throw new Error("Invalid Scout connection config");
19
- }
20
- if (!value || typeof value !== "object" || !("credentialsPath" in value)
21
- || typeof value.credentialsPath !== "string" || !value.credentialsPath.trim()) {
22
- throw new Error("Invalid Scout connection config");
23
- }
24
- return value.credentialsPath;
25
- }
26
- export function createScoutPiExtension(options = {}) {
27
- return (pi) => {
28
- pi.on("session_start", (_event, ctx) => {
29
- try {
30
- const credentials = readCredentials(options.credentialsPath ?? configuredCredentialsPath());
31
- if (!credentials)
32
- throw new Error("Scout credentials missing");
33
- pi.registerMcpServer("openmerit", {
34
- type: "http",
35
- url: `${credentials.server}/scout/mcp`,
36
- headers: { ...credentials.headers, Authorization: `Bearer ${credentials.key}` },
37
- exposure: "direct",
38
- });
39
- }
40
- catch {
41
- // Never surface parser, filesystem, or registration errors: they can
42
- // include credential/config contents. Setup is always an explicit act.
43
- ctx.ui.notify("OpenMerit Scout could not load its connection. Run `npx -y openmerit@latest install --client pi --scope user` "
44
- + "(use `--scope project` for a project installation), then start a new Pi session. "
45
- + "SDK hosts must also load Pi's createMcpExtension().", "warning");
46
- }
47
- });
48
- pi.on("before_agent_start", (event) => {
49
- // Mutate only our named sections; leave the host prompt, model, tool
50
- // allow/exclude lists, and all other extension sections untouched.
51
- event.systemPromptOptions.sections.openmerit_scout_rule = scoutRule("pi");
52
- event.systemPromptOptions.sections.openmerit_scout_skill = scoutSkill("pi");
53
- });
54
- };
55
- }
56
- export default createScoutPiExtension();
@@ -1 +0,0 @@
1
- {"format":1,"contract":"scout-recommend-v2","version":"0.3.11","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. First assess the\nproject's readiness for this task:\n\n- **No eval setup:** propose a customer-owned setup. Langfuse is the default\n suggestion for its supported aggregate connector, not a requirement: existing\n local/custom eval scripts can report aggregates without a hosted platform.\n Phoenix/Arize and Braintrust are alternatives, not supported Scout connectors.\n Ask which system the user wants before creating accounts or resources.\n- **Setup exists but evidence is insufficient:** keep that system. Write a\n task-specific eval plan in the project's local Markdown decision record for\n the user or their coding agent to implement. Name exact candidates/endpoints,\n quality floor and task/agent/outcome checks, labelled boundary cases and\n controls, sample size, early-stop rule, spend cap, aggregate mappings and\n temporary run retention. Explain what the current evidence cannot decide.\n- **Suitable evals exist:** reuse their fixtures and definitions; add only the\n approved candidate runs. Do not build a second eval or copy their datasets.\n\nOpenMerit does not host or run these evals. The customer or their coding agent\nimplements and maintains them with the customer's tools; Scout keeps Viewer-only\naccess. Ask before model spend or any platform write. Without measured evidence,\nScout may suggest candidates, but do not claim validated model quality.\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\n## Preserve evidence before cleanup\n\nBefore removing temporary comparison runs, save a local Markdown decision record\nwith the package/service versions, date, task and policy, exact candidates,\nunchanged controls, metric definitions/directions, sample counts, aggregate\nquality/cost/time, failures and exclusions, chosen model, limitations and next\nreview. Record regret against the cheapest measured candidate meeting the same\nquality floor; write \"not measurable\" when comparable evidence is missing.\nExclude secrets, private record IDs, prompts, outputs and trace bodies.\n\nTemporary comparison traces/records are not permanent: after the record is saved\nand checked, propose the exact newly created comparison runs to remove. Obtain\nexplicit scoped approval, including any cascading deletion, before the customer\nor their coding agent deletes them with customer-controlled write access. Scout's\nViewer key cannot do this. Never delete pre-existing customer records, reusable\nfixtures, dataset definitions or eval definitions. Verify the approved cleanup\nand retained controls, then record the outcome locally. If not approved, leave\nrecords unchanged and state cleanup is pending; do not imply it happened.\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.\nOnce the user has confirmed the connection and score/model mappings (including\na connection already recorded in the project), call `scout_models` with\n`sync: \"langfuse\"` on the first consultation and before a final model decision.\nThe CLI equivalent is `openmerit scout --conversation <id> --sync langfuse`.\nRefreshing that approved connection reads aggregates and updates Scout's cache;\nit does not run candidates or create/change records in Langfuse. Do not ask for\nnew spend or platform-write approval just to refresh it. Do not reconstruct and\nre-report old measurements from project notes instead of using the connection.\nIf connection status is unknown, check `openmerit connect langfuse --status`\nbefore proposing setup; application tracing keys alone do not prove a Scout\nconnection. If sync fails, disclose the failure and any use of cached evidence;\ndo not describe it as fresh. Never read trace bodies to work around missing\nmappings or untagged runs; use the aggregate report path for those gaps instead.\nResults 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"}
@@ -1,9 +0,0 @@
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';
@@ -1,28 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
-
3
- // Callers decide which URLs may open. Never use a shell.
4
- export function browserCommand(url, platform = process.platform, env = process.env, allowed) {
5
- if (!allowed(url)) throw new Error('Refusing to open an unexpected URL.');
6
- if (env.SSH_CONNECTION || env.SSH_TTY) return null;
7
- if (platform === 'darwin') return ['open', [url]];
8
- if (platform === 'win32') return ['rundll32.exe', ['url.dll,FileProtocolHandler', url]];
9
- if (platform === 'linux' && (env.DISPLAY || env.WAYLAND_DISPLAY)) return ['xdg-open', [url]];
10
- return null;
11
- }
12
-
13
- export async function openBrowser(url, { platform = process.platform, env = process.env, spawnProcess = spawn, timeoutMs = 3000, allowed = /** @type {(url: string) => boolean} */ (() => false) } = {}) {
14
- const command = browserCommand(url, platform, env, allowed);
15
- if (!command) return false;
16
- return new Promise(resolve => {
17
- let child, timer;
18
- const finish = success => { clearTimeout(timer); resolve(success); };
19
- try {
20
- child = spawnProcess(command[0], command[1], { env, stdio: 'ignore', shell: false, detached: true });
21
- child.once('error', () => finish(false));
22
- child.once('exit', code => finish(code === 0));
23
- // Some desktop openers stay attached to the browser; don't wait on its lifetime.
24
- timer = setTimeout(() => finish(true), timeoutMs);
25
- child.unref();
26
- } catch { finish(false); }
27
- });
28
- }
@@ -1,28 +0,0 @@
1
- #!/usr/bin/env node
2
- import { commandTelemetry } from './telemetry.mjs';
3
-
4
- const help = `OpenMerit
5
-
6
- openmerit scout --help Ask Scout which models fit your tasks
7
- openmerit install --help Connect OpenMerit to your coding agent
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
11
- openmerit --help Show this help
12
- `;
13
-
14
- async function main() {
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; }
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; }
18
- if (args[0] === 'scout') { const { main } = await import('./scout.js'); process.exitCode = await main(args.slice(1)); return; }
19
- if (!args.length || args.includes('--help') || args.includes('-h')) { console.log(help); return; }
20
- throw new Error('Unknown command. Run openmerit --help.');
21
- }
22
- const completeTelemetry = commandTelemetry(process.argv.slice(2));
23
- const setupCodes = { 10: 'approval_pending', 11: 'access_denied', 20: 'install_conflict', 30: 'openmerit_unreachable' };
24
- main().then(() => completeTelemetry(process.exitCode ? 'error' : 'ok', undefined, setupCodes[process.exitCode]), async error => {
25
- console.error(`\n ${error.message}\n`);
26
- process.exitCode = 1;
27
- await completeTelemetry('error', error);
28
- });
@@ -1,151 +0,0 @@
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
- For a new connection, supply LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY through
23
- a secret manager. Omit both to update settings using the saved Viewer key.
24
- --stdin reads the full JSON settings including the Viewer key pair from a protected
25
- pipe (maximum 18 KB), never echoes them, and does not save them locally.
26
- Never put keys in arguments, chat, shell history or a tracked file. Use a separate
27
- Viewer key, not tracing keys. OPENMERIT_CONNECTION_KEY can supply an already
28
- approved management key to unattended jobs; otherwise the saved key is used.
29
-
30
- Disconnect deletes the key pair and imports, not reported results or conversations.
31
- --delete-all also removes reported results. Both require --yes for headless use.
32
- Without --yes, disconnect opens the optional browser confirmation.
33
- `;
34
- const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
35
- async function settings(file, stdin, root) {
36
- let text = '';
37
- if (stdin) {
38
- if (process.stdin.isTTY)
39
- throw new Error('--stdin requires a protected JSON pipe, not interactive key entry.');
40
- process.stdin.setEncoding('utf8');
41
- let bytes = 0;
42
- for await (const chunk of process.stdin) {
43
- bytes += Buffer.byteLength(chunk);
44
- if (bytes > 18_000)
45
- throw new Error('Connection settings exceed 18 KB.');
46
- text += String(chunk);
47
- }
48
- }
49
- else {
50
- try {
51
- text = await readFile(resolve(root, file ?? ''), 'utf8');
52
- }
53
- catch {
54
- throw new Error('Could not read connection settings.');
55
- }
56
- }
57
- if (Buffer.byteLength(text) > 18_000)
58
- throw new Error('Connection settings exceed 18 KB.');
59
- let config;
60
- try {
61
- config = JSON.parse(text);
62
- }
63
- catch {
64
- throw new Error('Connection settings must be a JSON object.');
65
- }
66
- const fields = ['region', 'from', 'tagPrefix', 'mappings', 'models', ...(stdin ? ['publicKey', 'secretKey'] : [])];
67
- if (!object(config) || Object.keys(config).some(key => !fields.includes(key)))
68
- throw new Error('Use non-secret settings in --config; credentials must come from a secret manager or --stdin.');
69
- if (!stdin)
70
- config = { ...config, publicKey: process.env.LANGFUSE_PUBLIC_KEY, secretKey: process.env.LANGFUSE_SECRET_KEY };
71
- if (object(config) && !stdin && config.publicKey === undefined && config.secretKey === undefined) {
72
- delete config.publicKey;
73
- delete config.secretKey;
74
- return config;
75
- }
76
- if (!object(config) || typeof config.publicKey !== 'string' || typeof config.secretKey !== 'string' || !config.publicKey || !config.secretKey)
77
- throw new Error('Supply a separate Viewer key pair through a secret manager or --stdin.');
78
- return config;
79
- }
80
- export async function connection(args, { root = process.cwd(), print = console.log, printError = console.error } = {}) {
81
- if (args.includes('--help') || args.includes('-h')) {
82
- print(help);
83
- return 0;
84
- }
85
- const [command, provider, ...flags] = args;
86
- if (!['connect', 'disconnect'].includes(command ?? '') || provider !== 'langfuse')
87
- throw new Error('Use openmerit connect langfuse --help.');
88
- let file;
89
- const options = new Set();
90
- for (let i = 0; i < flags.length; i++) {
91
- const flag = flags[i] ?? '';
92
- if (options.has(flag))
93
- throw new Error('Duplicate connection option.');
94
- options.add(flag);
95
- if (flag === '--config') {
96
- file = flags[++i];
97
- if (!file || file.startsWith('-'))
98
- throw new Error('--config needs a settings file.');
99
- }
100
- else if (!['--stdin', '--authorize', '--no-open', '--status', '--sync', '--yes', '--delete-all', '--json'].includes(flag))
101
- throw new Error('Unknown connection option. Use --help; never pass keys as arguments.');
102
- }
103
- const operation = ['--config', '--stdin', '--authorize', '--status', '--sync'].filter(flag => options.has(flag));
104
- if (operation.length > 1 || command === 'disconnect' && operation.length || command === 'connect' && (options.has('--yes') || options.has('--delete-all'))
105
- || options.has('--no-open') && !options.has('--authorize'))
106
- throw new Error('Choose one connection action. Use --help.');
107
- if (options.has('--delete-all') && !options.has('--yes'))
108
- throw new Error('--delete-all requires --yes.');
109
- if (options.has('--authorize'))
110
- return install(['login', '--client', 'connector', '--scope', 'user', ...(options.has('--no-open') ? ['--no-open'] : []), ...(options.has('--json') ? ['--json'] : [])], { root, print, printError });
111
- if (!operation.length && !options.has('--yes')) {
112
- const url = 'https://openmerit.ai/connect/?integration=langfuse';
113
- print(`Manage Langfuse in your signed-in browser: ${url}`);
114
- await openBrowser(url, { allowed: (value) => value === url }).catch(() => false);
115
- return 0;
116
- }
117
- const server = (process.env.OPENMERIT_URL ?? 'https://openmerit.ai').replace(/\/+$/, '');
118
- const url = new URL(server);
119
- 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))))
120
- throw new Error('Use an HTTPS OpenMerit origin, or localhost for development.');
121
- const credentials = readCredentials(credentialsPath('connector', 'user', root));
122
- const key = process.env.OPENMERIT_CONNECTION_KEY ?? (credentials?.server === server ? credentials.key : undefined);
123
- if (!key || !/^om_[A-Za-z0-9_-]{43}$/.test(key))
124
- throw new Error('Authorize connection management first: openmerit connect langfuse --authorize --no-open');
125
- // Read secrets only after authorization. No credential values reach stdout,
126
- // local files, tool arguments, analytics or errors, even on malformed input.
127
- const config = file || options.has('--stdin') ? await settings(file, options.has('--stdin'), root) : undefined;
128
- const path = options.has('--delete-all') ? 'evaluations' : options.has('--sync') ? 'langfuse/sync' : 'langfuse';
129
- const method = command === 'disconnect' ? 'DELETE' : config || options.has('--sync') ? 'POST' : 'GET';
130
- let response;
131
- try {
132
- response = await fetch(`${server}/connect/api/${path}`, { method, redirect: 'error', signal: AbortSignal.timeout(120_000),
133
- headers: { Authorization: `Bearer ${key}`, ...(config ? { 'Content-Type': 'application/json' } : {}), 'X-Scout-Analytics': 'off' },
134
- ...(config ? { body: JSON.stringify(config) } : {}),
135
- });
136
- }
137
- catch {
138
- throw new Error('Could not reach OpenMerit. Check connection status before retrying a change.');
139
- }
140
- if (!response.ok) {
141
- await response.body?.cancel();
142
- throw new Error(response.status === 401 || response.status === 403 ? 'Management key rejected. Run openmerit connect langfuse --authorize --no-open.'
143
- : `Connection request failed (HTTP ${response.status}). Check Viewer permissions, region and mappings; check status before retrying.`);
144
- }
145
- const value = await response.json();
146
- if (!object(value))
147
- throw new Error('OpenMerit returned an invalid connection response.');
148
- const state = Object.fromEntries(Object.entries(value).filter(([name]) => ['connected', 'connectedAt', 'syncedAt', 'mappingCount', 'modelAliasCount', 'experiments', 'unmapped', 'results', 'busy', 'cached', 'deleted'].includes(name)));
149
- print(JSON.stringify(state, null, 2));
150
- return 0;
151
- }
@@ -1,2 +0,0 @@
1
- export function main(args: any, dependencies?: {}): Promise<any>;
2
- export { rule, skill } from "./scout-instructions.ts";