openmerit 0.3.11 → 0.4.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/README.md +74 -22
- package/data/eci-estimates.csv +179 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +180 -0
- package/dist/connectors/langfuse/client.d.ts +42 -0
- package/dist/connectors/langfuse/client.js +141 -0
- package/dist/connectors/langfuse/index.d.ts +163 -0
- package/dist/connectors/langfuse/index.js +378 -0
- package/dist/connectors/langfuse/otlp.d.ts +60 -0
- package/dist/connectors/langfuse/otlp.js +40 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/pi/extension.d.ts +13 -0
- package/dist/pi/extension.js +148 -0
- package/dist/pi/harness.d.ts +20 -0
- package/dist/pi/harness.js +49 -0
- package/dist/pi/instructions.d.ts +2 -0
- package/dist/pi/instructions.js +12 -0
- package/dist/scout/candidates.d.ts +62 -0
- package/dist/scout/candidates.js +90 -0
- package/dist/scout/eci.d.ts +42 -0
- package/dist/scout/eci.js +84 -0
- package/dist/scout/evaluators/module.d.ts +24 -0
- package/dist/scout/evaluators/module.js +19 -0
- package/dist/scout/evaluators/pi.d.ts +53 -0
- package/dist/scout/evaluators/pi.js +117 -0
- package/dist/scout/pipeline.d.ts +111 -0
- package/dist/scout/pipeline.js +157 -0
- package/dist/scout/report.d.ts +3 -0
- package/dist/scout/report.js +42 -0
- package/dist/scout/search.d.ts +110 -0
- package/dist/scout/search.js +205 -0
- package/dist/scout/service.d.ts +148 -0
- package/dist/scout/service.js +143 -0
- package/examples/health-declaration-codes/dataset.jsonl +14 -0
- package/examples/sahra-enrollment/evaluator.mts +152 -0
- package/package.json +34 -78
- package/CHANGELOG.md +0 -255
- package/dist/pi/scout-extension.mjs +0 -287
- package/dist/pi/src/index.d.ts +0 -6
- package/dist/pi/src/index.js +0 -56
- package/dist/terminal/scout-update.json +0 -1
- package/dist/terminal/src/browser.d.mts +0 -9
- package/dist/terminal/src/browser.mjs +0 -28
- package/dist/terminal/src/cli.mjs +0 -28
- package/dist/terminal/src/connections.js +0 -151
- package/dist/terminal/src/install.d.mts +0 -2
- package/dist/terminal/src/install.mjs +0 -524
- package/dist/terminal/src/scout-claude.js +0 -25
- package/dist/terminal/src/scout-credentials.js +0 -41
- package/dist/terminal/src/scout-instructions.js +0 -213
- package/dist/terminal/src/scout-pi.js +0 -84
- package/dist/terminal/src/scout.js +0 -229
- package/dist/terminal/src/telemetry.d.mts +0 -30
- package/dist/terminal/src/telemetry.mjs +0 -138
- package/dist/terminal/src/updates.js +0 -397
- package/docs/agent-evaluations.md +0 -15
- package/docs/commands.md +0 -9
- package/docs/getting-started.md +0 -37
- package/docs/index.md +0 -25
- package/docs/inference-terminal.md +0 -25
- package/docs/pi-extension.md +0 -58
- package/docs/roadmap.md +0 -16
- package/docs/scout-close-loop.md +0 -371
- package/docs/scout-data-policy.md +0 -87
- package/docs/scout.md +0 -237
- package/docs/security.md +0 -107
- package/docs/testing.md +0 -114
- package/docs/troubleshooting.md +0 -50
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
export const rule = `# Application model decisions · OpenMerit Scout
|
|
2
|
-
|
|
3
|
-
Before choosing, recommending, adding, downloading, replacing or configuring
|
|
4
|
-
ANY application inference model, load the \`openmerit-scout\` skill and consult
|
|
5
|
-
\`scout_models\` on the \`openmerit\` MCP server before committing to a model.
|
|
6
|
-
This applies even when the user only asks you to build a feature, never mentions
|
|
7
|
-
models or Scout, or names a model themselves (assess it; do not silently replace it).
|
|
8
|
-
|
|
9
|
-
A pretrained model, local weights, browser inference, an SDK's default model,
|
|
10
|
-
embeddings, OCR, speech, image classification, detection and segmentation all
|
|
11
|
-
count as model choices, not implementation details. Identify this decision before
|
|
12
|
-
writing model-specific code, installing a model package or downloading weights.
|
|
13
|
-
|
|
14
|
-
Follow the skill's project access policy. If it is missing, ask and save it first.
|
|
15
|
-
Scout may answer with clarifying questions: relay them to the user and continue
|
|
16
|
-
the same Scout conversation with their answers before choosing.
|
|
17
|
-
If Scout is unavailable, fails or has no suitable evidence/candidate, disclose the
|
|
18
|
-
gap and pause the model decision; do not fall back to a remembered or SDK-default
|
|
19
|
-
model. You may continue work that does not commit to a model.
|
|
20
|
-
|
|
21
|
-
Do not consult Scout for unrelated edits, normal inference calls using an unchanged
|
|
22
|
-
approved model, or this coding agent's own model. Do not reselect approved models
|
|
23
|
-
unless the task introduces or changes a model decision.
|
|
24
|
-
|
|
25
|
-
When the user says "update openmerit", run
|
|
26
|
-
\`npx -y openmerit@latest update --scope user\` (use \`--scope project\` if this
|
|
27
|
-
rule is in the project's .claude/rules directory). Report the result, including
|
|
28
|
-
any conflict or manual-upgrade requirement. Do not delete edited files, bypass
|
|
29
|
-
permissions or rerun login automatically. A new session loads updated instructions.
|
|
30
|
-
`;
|
|
31
|
-
export const skill = `---
|
|
32
|
-
name: openmerit-scout
|
|
33
|
-
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.
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
# OpenMerit · Scout
|
|
37
|
-
|
|
38
|
-
Before introducing, recommending, replacing or configuring an application
|
|
39
|
-
inference model, call the \`scout_models\` tool on the \`openmerit\` MCP server.
|
|
40
|
-
This applies even when the user never mentions models or Scout. Downloading local
|
|
41
|
-
weights, using a pretrained browser model or accepting an SDK default is a model
|
|
42
|
-
decision: consult Scout before model-specific code, installs or downloads.
|
|
43
|
-
Preserve the user's explicit model choice; assess it rather than silently replacing
|
|
44
|
-
it. Leave unchanged, approved application selections in place.
|
|
45
|
-
|
|
46
|
-
## Project policy
|
|
47
|
-
|
|
48
|
-
Read \`.openmerit/scout.json\` before searching. If it is absent, ask once whether this
|
|
49
|
-
project uses **existing-only** (only endpoints it can already reach) or
|
|
50
|
-
**existing-first** (prefer them, ask before searching the whole catalogue), and
|
|
51
|
-
which public catalogue endpoint IDs it can access. Save the answer:
|
|
52
|
-
|
|
53
|
-
\`\`\`sh
|
|
54
|
-
npx -y openmerit@latest project --policy existing-only --endpoint-id <id> ...
|
|
55
|
-
\`\`\`
|
|
56
|
-
|
|
57
|
-
Never infer access from a model name, SDK or environment-variable name. Never
|
|
58
|
-
record credentials, private URLs, prompts or customer data. An empty inventory
|
|
59
|
-
is not permission to search all models.
|
|
60
|
-
|
|
61
|
-
## Searching
|
|
62
|
-
|
|
63
|
-
Call \`scout_models\` with a non-confidential task description as \`prompt\` and
|
|
64
|
-
\`endpointIds\` set to the saved inventory, including \`[]\` when empty. Send
|
|
65
|
-
only task requirements, never source files, secrets or real customer records.
|
|
66
|
-
If the MCP tool is unavailable, run
|
|
67
|
-
\`npx -y openmerit@latest scout "<task>" --json\`; it applies the saved inventory.
|
|
68
|
-
|
|
69
|
-
Scout is a conversation with the agent behind openmerit.ai/scout. When
|
|
70
|
-
\`status\` is \`needs_answers\`, fill in what the user or project already states,
|
|
71
|
-
show the user Scout's brief and remaining questions, and wait for their answers.
|
|
72
|
-
Then call again with the returned \`conversationId\` and \`brief\` set to the
|
|
73
|
-
rewritten components, or \`skip: true\` if the user wants Scout to assume. Never
|
|
74
|
-
answer Scout's questions with guesses. Send later refinements to the same
|
|
75
|
-
\`conversationId\`. A call with only a \`conversationId\` reads an interrupted turn.
|
|
76
|
-
|
|
77
|
-
When \`status\` is \`recommended\`, Scout has split the work into \`tasks\`, one
|
|
78
|
-
per model, each with up to five ranked \`models\`; cover every task. Each
|
|
79
|
-
model's \`cost\` names the \`endpointId\` and \`providerModelId\` to call.
|
|
80
|
-
Existing-only never broadens. For existing-first, if no existing candidate fits a
|
|
81
|
-
task, explain the gap and ask before a new conversation without \`endpointIds\`.
|
|
82
|
-
Compare quality evidence, reference cost/time and watch-outs. Prefer the cheapest
|
|
83
|
-
usable option the evidence supports, without claiming benchmarks prove
|
|
84
|
-
application accuracy. Use the project's actual SDK and protocol.
|
|
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. First assess the
|
|
91
|
-
project's readiness for this task:
|
|
92
|
-
|
|
93
|
-
- **No eval setup:** propose a customer-owned setup. Langfuse is the default
|
|
94
|
-
suggestion for its supported aggregate connector, not a requirement: existing
|
|
95
|
-
local/custom eval scripts can report aggregates without a hosted platform.
|
|
96
|
-
Phoenix/Arize and Braintrust are alternatives, not supported Scout connectors.
|
|
97
|
-
Ask which system the user wants before creating accounts or resources.
|
|
98
|
-
- **Setup exists but evidence is insufficient:** keep that system. Write a
|
|
99
|
-
task-specific eval plan in the project's local Markdown decision record for
|
|
100
|
-
the user or their coding agent to implement. Name exact candidates/endpoints,
|
|
101
|
-
quality floor and task/agent/outcome checks, labelled boundary cases and
|
|
102
|
-
controls, sample size, early-stop rule, spend cap, aggregate mappings and
|
|
103
|
-
temporary run retention. Explain what the current evidence cannot decide.
|
|
104
|
-
- **Suitable evals exist:** reuse their fixtures and definitions; add only the
|
|
105
|
-
approved candidate runs. Do not build a second eval or copy their datasets.
|
|
106
|
-
|
|
107
|
-
OpenMerit does not host or run these evals. The customer or their coding agent
|
|
108
|
-
implements and maintains them with the customer's tools; Scout keeps Viewer-only
|
|
109
|
-
access. Ask before model spend or any platform write. Without measured evidence,
|
|
110
|
-
Scout may suggest candidates, but do not claim validated model quality.
|
|
111
|
-
|
|
112
|
-
Report what you measured with \`scout_models\`: the same \`conversationId\` and
|
|
113
|
-
\`results\`, one aggregate per model and metric (\`level\`, \`task\` title, \`scope\`,
|
|
114
|
-
\`model\` endpoint or model ID, \`metric\`, \`value\`, \`higherIsBetter\`,
|
|
115
|
-
\`samples\`, \`source\`). Set \`kind\` to \`usd\` or \`seconds\` for measured cost
|
|
116
|
-
or time (per call at the task level, per run at the agent level), so Scout uses
|
|
117
|
-
your cost instead of its estimate. Report each agent's own checks at the agent
|
|
118
|
-
level: an end-to-end result also measures the agents you did not swap. Send
|
|
119
|
-
aggregates only, never inputs, outputs or records.
|
|
120
|
-
Scout ranks them above public benchmarks for this user only; models whose
|
|
121
|
-
\`quality.measured\` is true rest on them.
|
|
122
|
-
|
|
123
|
-
Swap one agent at a time, holding the other agents, prompts, fixtures and controls
|
|
124
|
-
fixed. Use each agent's own checks to attribute failures. If all candidates score
|
|
125
|
-
100%, the eval cannot separate their quality: add harder labelled cases before
|
|
126
|
-
claiming equivalent quality. Cost may break the observed tie, not prove equality.
|
|
127
|
-
|
|
128
|
-
## Preserve evidence before cleanup
|
|
129
|
-
|
|
130
|
-
Before removing temporary comparison runs, save a local Markdown decision record
|
|
131
|
-
with the package/service versions, date, task and policy, exact candidates,
|
|
132
|
-
unchanged controls, metric definitions/directions, sample counts, aggregate
|
|
133
|
-
quality/cost/time, failures and exclusions, chosen model, limitations and next
|
|
134
|
-
review. Record regret against the cheapest measured candidate meeting the same
|
|
135
|
-
quality floor; write "not measurable" when comparable evidence is missing.
|
|
136
|
-
Exclude secrets, private record IDs, prompts, outputs and trace bodies.
|
|
137
|
-
|
|
138
|
-
Temporary comparison traces/records are not permanent: after the record is saved
|
|
139
|
-
and checked, propose the exact newly created comparison runs to remove. Obtain
|
|
140
|
-
explicit scoped approval, including any cascading deletion, before the customer
|
|
141
|
-
or their coding agent deletes them with customer-controlled write access. Scout's
|
|
142
|
-
Viewer key cannot do this. Never delete pre-existing customer records, reusable
|
|
143
|
-
fixtures, dataset definitions or eval definitions. Verify the approved cleanup
|
|
144
|
-
and retained controls, then record the outcome locally. If not approved, leave
|
|
145
|
-
records unchanged and state cleanup is pending; do not imply it happened.
|
|
146
|
-
|
|
147
|
-
If the user has Langfuse, use \`openmerit connect langfuse --help\`: the web UI is
|
|
148
|
-
optional. Headless setup requires a separate connection-manager approval with
|
|
149
|
-
\`openmerit connect langfuse --authorize --no-open\`. Relay its approval link and
|
|
150
|
-
expiry; after approval, rerun that command. Existing recommendation keys cannot
|
|
151
|
-
manage connections. The user creates a separate Viewer key; receive it through
|
|
152
|
-
protected stdin or a secret-manager-injected environment, never chat, tool
|
|
153
|
-
arguments, shell literals, traced commands or tracked files. The CLI accepts
|
|
154
|
-
non-secret settings via \`--config mappings.json\` and reads LANGFUSE_PUBLIC_KEY
|
|
155
|
-
and LANGFUSE_SECRET_KEY from its environment, or reads full settings via --stdin.
|
|
156
|
-
A separate trusted MCP connection with the management key exposes
|
|
157
|
-
\`scout_connection\` (status/configure/sync/disconnect/clear). For configure, keys
|
|
158
|
-
must come from client-controlled HTTP headers outside model context; if the
|
|
159
|
-
client cannot do this securely, use protected CLI input or the web UI instead.
|
|
160
|
-
Once the user has confirmed the connection and score/model mappings (including
|
|
161
|
-
a connection already recorded in the project), call \`scout_models\` with
|
|
162
|
-
\`sync: "langfuse"\` on the first consultation and before a final model decision.
|
|
163
|
-
The CLI equivalent is \`openmerit scout --conversation <id> --sync langfuse\`.
|
|
164
|
-
Refreshing that approved connection reads aggregates and updates Scout's cache;
|
|
165
|
-
it does not run candidates or create/change records in Langfuse. Do not ask for
|
|
166
|
-
new spend or platform-write approval just to refresh it. Do not reconstruct and
|
|
167
|
-
re-report old measurements from project notes instead of using the connection.
|
|
168
|
-
If connection status is unknown, check \`openmerit connect langfuse --status\`
|
|
169
|
-
before proposing setup; application tracing keys alone do not prove a Scout
|
|
170
|
-
connection. If sync fails, disclose the failure and any use of cached evidence;
|
|
171
|
-
do not describe it as fresh. Never read trace bodies to work around missing
|
|
172
|
-
mappings or untagged runs; use the aggregate report path for those gaps instead.
|
|
173
|
-
Results are stored per user, used in new conversations and
|
|
174
|
-
expire after 90 days. Disconnect deletes the Viewer key and imported aggregates;
|
|
175
|
-
Delete all evaluation data also deletes manually reported aggregates. Obtain
|
|
176
|
-
consent before deleting: headless CLI requires --yes, MCP requires confirm: true.
|
|
177
|
-
Deleting the connection does not revoke the separate management device key.
|
|
178
|
-
Past conversation text is not erased. Never put record IDs, emails or secrets in
|
|
179
|
-
\`scope\` or \`source\`; use labels such as "coding agent" and "held-out eval".
|
|
180
|
-
|
|
181
|
-
## Boundaries
|
|
182
|
-
|
|
183
|
-
Ask before adding providers or credentials, spending, deploying or external
|
|
184
|
-
writes. Report recommendation, endpoint connectivity and application checks as
|
|
185
|
-
separate results, each only after it succeeds. Do not switch this coding agent's
|
|
186
|
-
own model.
|
|
187
|
-
|
|
188
|
-
If Scout fails or has no suitable candidate/evidence for the required deployment,
|
|
189
|
-
say so and pause the model decision; do not substitute a remembered recommendation
|
|
190
|
-
or SDK default. Continue only model-independent work. Local/browser models are not
|
|
191
|
-
exempt when the catalogue lacks coverage. When an install or login waits for approval (exit code
|
|
192
|
-
10), show the user the approval link and its UTC expiry in your reply, not only in
|
|
193
|
-
command output, then rerun the same command once they approve. If it reports that the device key is missing,
|
|
194
|
-
expired or revoked, run \`npx -y openmerit@latest login\` and then
|
|
195
|
-
\`npx -y openmerit@latest install\` again. Never ask the user to paste a key.
|
|
196
|
-
`;
|
|
197
|
-
export function scoutRule(client) {
|
|
198
|
-
if (client === 'claude-code')
|
|
199
|
-
return rule;
|
|
200
|
-
return rule.replace('load the `openmerit-scout` skill and consult', 'follow the Scout project policy below and consult')
|
|
201
|
-
.replaceAll('`scout_models`', '`mcp__openmerit__scout_models`')
|
|
202
|
-
.replace("Follow the skill's project access policy.", 'Follow the project access policy below.')
|
|
203
|
-
.replace('update --scope user', 'install --client pi --scope user')
|
|
204
|
-
.replace("rule is in the project's .claude/rules directory", "Pi installation is project-scoped");
|
|
205
|
-
}
|
|
206
|
-
export function scoutSkill(client) {
|
|
207
|
-
if (client === 'claude-code')
|
|
208
|
-
return skill;
|
|
209
|
-
return skill.replaceAll('`scout_models`', '`mcp__openmerit__scout_models`')
|
|
210
|
-
.replace('openmerit@latest login', 'openmerit@latest login --client pi')
|
|
211
|
-
.replace('openmerit@latest install`', 'openmerit@latest install --client pi`')
|
|
212
|
-
.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.');
|
|
213
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { readFile, mkdir, writeFile, rm } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { scoutSkill } from "./scout-instructions.js";
|
|
6
|
-
export const piVersion = '1.5.0';
|
|
7
|
-
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
8
|
-
const optional = async (path) => {
|
|
9
|
-
try {
|
|
10
|
-
return await readFile(path, 'utf8');
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
14
|
-
return null;
|
|
15
|
-
throw error;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
async function object(path) {
|
|
19
|
-
const value = JSON.parse(await optional(path) ?? '{}');
|
|
20
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
21
|
-
throw new Error(`Invalid Scout installation metadata at ${path}; nothing changed.`);
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
|
-
export function piPaths(root, scope) {
|
|
25
|
-
const agentDir = resolve(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi/agent'));
|
|
26
|
-
const base = scope === 'user' ? agentDir : join(root, '.pi');
|
|
27
|
-
return { skill: join(base, 'skills/openmerit-scout/SKILL.md'), rule: join(base, 'extensions/openmerit-scout.ts'),
|
|
28
|
-
connection: join(base, 'extensions/openmerit-scout.json'), record: join(base, 'openmerit-install.json'),
|
|
29
|
-
mcpFile: join(base, 'mcp.json'), mcpScope: scope, agentDir };
|
|
30
|
-
}
|
|
31
|
-
export async function inspectPi(root, scope) {
|
|
32
|
-
const paths = piPaths(root, scope);
|
|
33
|
-
const otherPaths = piPaths(root, scope === 'user' ? 'project' : 'user');
|
|
34
|
-
const [record, connection, installedSkill, installedRule, mcp, otherMcp, other] = await Promise.all([
|
|
35
|
-
object(paths.record), optional(paths.connection), optional(paths.skill), optional(paths.rule), object(paths.mcpFile),
|
|
36
|
-
object(otherPaths.mcpFile), optional(otherPaths.rule),
|
|
37
|
-
]);
|
|
38
|
-
const owned = record.version === 1 && record.client === 'pi' && record.scope === scope;
|
|
39
|
-
const matches = (value, field) => value === null || owned && [record[field], record[`pending${field[0].toUpperCase()}${field.slice(1)}`]].includes(hash(value));
|
|
40
|
-
return { paths, record, owned, current: connection, installedSkill, installedRule,
|
|
41
|
-
serverOwned: matches(connection, 'serverHash'), skillOwned: matches(installedSkill, 'skillHash'), ruleOwned: matches(installedRule, 'ruleHash'),
|
|
42
|
-
updates: { saved: null, active: false, conflicts: [] }, legacyRule: 'none',
|
|
43
|
-
shadowed: other !== null || [mcp, otherMcp].some(config => config.mcpServers && typeof config.mcpServers === 'object' && 'openmerit' in config.mcpServers), };
|
|
44
|
-
}
|
|
45
|
-
export function piConflicts(state) {
|
|
46
|
-
return [
|
|
47
|
-
...(!state.serverOwned ? [`${state.paths.connection} was edited or is unowned.`] : []),
|
|
48
|
-
...(!state.skillOwned ? [`${state.paths.skill} was edited or is unowned.`] : []),
|
|
49
|
-
...(!state.ruleOwned ? [`${state.paths.rule} was edited or is unowned.`] : []),
|
|
50
|
-
...(state.shadowed ? ['Another Pi Scout installation or an openmerit MCP entry shadows this installation. Resolve the duplicate before installing.'] : []),
|
|
51
|
-
];
|
|
52
|
-
}
|
|
53
|
-
export async function configurePi(root, scope, credentialsPath, ids) {
|
|
54
|
-
const state = await inspectPi(root, scope);
|
|
55
|
-
const conflicts = piConflicts(state);
|
|
56
|
-
if (conflicts.length)
|
|
57
|
-
throw new Error(conflicts.join('\n'));
|
|
58
|
-
const extension = await readFile(new URL('../../../dist/pi/scout-extension.mjs', import.meta.url), 'utf8');
|
|
59
|
-
const connection = `${JSON.stringify({ credentialsPath }, null, 2)}\n`;
|
|
60
|
-
const skill = scoutSkill('pi');
|
|
61
|
-
const next = { version: 1, client: 'pi', scope, ...ids, serverHash: hash(connection), skillHash: hash(skill), ruleHash: hash(extension), installedAt: new Date().toISOString() };
|
|
62
|
-
const save = async (path, value) => {
|
|
63
|
-
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
64
|
-
await writeFile(path, typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
65
|
-
};
|
|
66
|
-
// Record intent before writing files, so interrupted installs are recoverable.
|
|
67
|
-
await save(state.paths.record, { ...state.record, version: 1, client: 'pi', scope,
|
|
68
|
-
pendingServerHash: next.serverHash, pendingSkillHash: next.skillHash, pendingRuleHash: next.ruleHash });
|
|
69
|
-
await save(state.paths.connection, connection);
|
|
70
|
-
await save(state.paths.skill, skill);
|
|
71
|
-
await save(state.paths.rule, extension);
|
|
72
|
-
await save(state.paths.record, next);
|
|
73
|
-
}
|
|
74
|
-
export async function removePi(state) {
|
|
75
|
-
if (state.current !== null && state.serverOwned)
|
|
76
|
-
await rm(state.paths.connection, { force: true });
|
|
77
|
-
if (state.installedRule !== null && state.ruleOwned)
|
|
78
|
-
await rm(state.paths.rule, { force: true });
|
|
79
|
-
if (state.installedSkill !== null && state.skillOwned)
|
|
80
|
-
await rm(state.paths.skill, { force: true });
|
|
81
|
-
// Keep ownership of edited files, so a future install does not adopt them.
|
|
82
|
-
if (state.serverOwned && state.ruleOwned && state.skillOwned)
|
|
83
|
-
await rm(state.paths.record, { force: true });
|
|
84
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
// `openmerit scout`: a conversation with Scout, the agent behind openmerit.ai/scout,
|
|
2
|
-
// for coding agents without the MCP server and for people at a terminal. It calls
|
|
3
|
-
// the same scout_models MCP tool with this device's key, so every client gets the
|
|
4
|
-
// same clarifying questions and per-task shortlists.
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
6
|
-
import { join, resolve } from 'node:path';
|
|
7
|
-
import { credentialsPath, readCredentials } from "./scout-credentials.js";
|
|
8
|
-
const policyFile = '.openmerit/scout.json';
|
|
9
|
-
export const components = ['input', 'work', 'output', 'criteria', 'constraints'];
|
|
10
|
-
const help = `OpenMerit · Scout
|
|
11
|
-
|
|
12
|
-
openmerit scout "<the work models need to do>" [--all-endpoints] [--json]
|
|
13
|
-
openmerit scout --conversation <id> [--brief <component>=<text> ...] [--skip] ["<follow-up>"] [--json]
|
|
14
|
-
openmerit scout --conversation <id> --results <file.json> [--json]
|
|
15
|
-
openmerit scout --conversation <id> --sync langfuse [--json]
|
|
16
|
-
openmerit connect langfuse
|
|
17
|
-
openmerit disconnect langfuse
|
|
18
|
-
|
|
19
|
-
Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
|
|
20
|
-
brief with clarifying questions: answer them with --brief (components: input,
|
|
21
|
-
work, output, criteria, constraints) in the same conversation, or --skip to let
|
|
22
|
-
Scout assume. It then splits the work into tasks and recommends up to five
|
|
23
|
-
models for each, with an evaluation: how to measure the candidates with your
|
|
24
|
-
own evals. Report what you measured with --results: a JSON list of aggregate
|
|
25
|
-
results ({level, kind?, task?, scope, model, metric, value, higherIsBetter,
|
|
26
|
-
samples, source}), never inputs or outputs. kind is quality (the default), usd
|
|
27
|
-
or seconds; measured usd replaces Scout's cost estimate. Scout ranks them above
|
|
28
|
-
public benchmarks, for your recommendations only. A turn usually takes one to three minutes.
|
|
29
|
-
|
|
30
|
-
Connect a separate Langfuse Viewer key through CLI or the optional web UI (see
|
|
31
|
-
openmerit connect langfuse --help), then --sync langfuse refreshes mapped, tagged
|
|
32
|
-
experiment aggregates before recommending.
|
|
33
|
-
Results are stored per user across conversations for 90 days. Disconnect deletes
|
|
34
|
-
the key and imports; use Delete all evaluation data to remove reported results too.
|
|
35
|
-
Past conversations remain. Never put record IDs or emails in source or scope.
|
|
36
|
-
|
|
37
|
-
A new conversation is limited to the endpoint IDs in ${policyFile}
|
|
38
|
-
(set with openmerit project). --all-endpoints searches the whole catalogue and is
|
|
39
|
-
refused under the existing-only policy. Uses the device key that
|
|
40
|
-
"openmerit install" saved for Claude Code or Pi, this project's first. Never include secrets, source files or customer records.
|
|
41
|
-
`;
|
|
42
|
-
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
43
|
-
async function readJSON(path) {
|
|
44
|
-
try {
|
|
45
|
-
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
46
|
-
return isObject(value) ? value : null;
|
|
47
|
-
}
|
|
48
|
-
catch (error) {
|
|
49
|
-
if (error.code === 'ENOENT')
|
|
50
|
-
return null;
|
|
51
|
-
throw new Error(`Could not read ${path}.`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// The device key an installation saved: this project's installation first, then the user's.
|
|
55
|
-
function deviceKey(root) {
|
|
56
|
-
for (const scope of ['project', 'user'])
|
|
57
|
-
for (const client of ['claude-code', 'pi']) {
|
|
58
|
-
const credentials = readCredentials(credentialsPath(client, scope, root));
|
|
59
|
-
if (credentials)
|
|
60
|
-
return credentials;
|
|
61
|
-
}
|
|
62
|
-
return null;
|
|
63
|
-
}
|
|
64
|
-
// One turn through the scout_models MCP tool. Errors are written for the person or agent running it.
|
|
65
|
-
async function askScout(root, request, signal) {
|
|
66
|
-
const credentials = deviceKey(root);
|
|
67
|
-
if (!credentials)
|
|
68
|
-
throw new Error('This device is not signed in to OpenMerit. Run: npx -y openmerit@latest install');
|
|
69
|
-
const server = credentials.server;
|
|
70
|
-
let response;
|
|
71
|
-
try {
|
|
72
|
-
response = await fetch(`${server}/scout/mcp`, {
|
|
73
|
-
method: 'POST', redirect: 'error',
|
|
74
|
-
// A turn can take minutes; Scout keeps working if the connection drops.
|
|
75
|
-
signal: AbortSignal.any([AbortSignal.timeout(10 * 60_000), ...(signal ? [signal] : [])]),
|
|
76
|
-
headers: { ...credentials.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${credentials.key}`, 'X-Scout-Client': 'cli' },
|
|
77
|
-
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'scout_models', arguments: request } }),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
catch (error) {
|
|
81
|
-
const id = request.conversationId;
|
|
82
|
-
throw new Error(error.name === 'TimeoutError' && id ? `Scout is still working. Read the outcome with: openmerit scout --conversation ${id}` : `Could not reach ${server}. Check your connection and retry.`);
|
|
83
|
-
}
|
|
84
|
-
if (response.status === 401)
|
|
85
|
-
throw new Error('The device key was rejected. Run: npx -y openmerit@latest login, then install again.');
|
|
86
|
-
const body = await response.json().catch(() => null);
|
|
87
|
-
const result = isObject(body) && isObject(body.result) ? body.result : null;
|
|
88
|
-
if (!result)
|
|
89
|
-
throw new Error(`Scout returned HTTP ${response.status}${isObject(body) && isObject(body.error) ? `: ${String(body.error.message)}` : ''}.`);
|
|
90
|
-
const text = Array.isArray(result.content) && isObject(result.content[0]) ? String(result.content[0].text) : '';
|
|
91
|
-
if (result.isError && !isObject(result.structuredContent))
|
|
92
|
-
throw new Error(text || 'Scout could not finish this turn.');
|
|
93
|
-
return result.structuredContent;
|
|
94
|
-
}
|
|
95
|
-
// A turn under the project's access policy. A new conversation is limited to the saved
|
|
96
|
-
// inventory; the whole catalogue needs an explicit choice the policy permits.
|
|
97
|
-
export async function scoutTurn(root, request, signal) {
|
|
98
|
-
const { allEndpoints, ...turn } = request;
|
|
99
|
-
if (turn.conversationId) {
|
|
100
|
-
if (allEndpoints)
|
|
101
|
-
throw new Error('--all-endpoints applies when a conversation starts. Start a new conversation instead.');
|
|
102
|
-
return askScout(root, turn, signal);
|
|
103
|
-
}
|
|
104
|
-
const policy = await readJSON(join(root, policyFile));
|
|
105
|
-
if (!policy) {
|
|
106
|
-
if (!allEndpoints)
|
|
107
|
-
throw new Error(`${policyFile} is missing. Ask which endpoints this project can use and save them with "npx -y openmerit@latest project --policy existing-only|existing-first --endpoint-id <id> ...", or pass --all-endpoints to search the whole catalogue.`);
|
|
108
|
-
return askScout(root, turn, signal);
|
|
109
|
-
}
|
|
110
|
-
const ids = Array.isArray(policy.endpointIds) && policy.endpointIds.every(id => typeof id === 'string') ? policy.endpointIds : null;
|
|
111
|
-
if (!ids || !['existing-only', 'existing-first'].includes(String(policy.policy)))
|
|
112
|
-
throw new Error(`${policyFile} is not a valid Scout policy. Save it again with "openmerit project".`);
|
|
113
|
-
if (allEndpoints && policy.policy === 'existing-only')
|
|
114
|
-
throw new Error('This project uses the existing-only policy, so Scout cannot search the whole catalogue.');
|
|
115
|
-
return askScout(root, allEndpoints ? turn : { ...turn, endpointIds: ids }, signal);
|
|
116
|
-
}
|
|
117
|
-
const money = (usd) => `$${usd < 0.1 ? usd.toFixed(3) : usd.toFixed(2)}`;
|
|
118
|
-
const rankings = { quality: 'Highest quality estimate first.', latency: 'Fastest first.', recency: 'Newest first.' };
|
|
119
|
-
export function renderScout(result) {
|
|
120
|
-
const lines = [];
|
|
121
|
-
if (result.status === 'needs_answers') {
|
|
122
|
-
lines.push('Scout needs more detail. Its task brief:', '');
|
|
123
|
-
for (const c of components) {
|
|
124
|
-
const part = result.brief?.[c];
|
|
125
|
-
lines.push(` ${c.padEnd(12)}${part?.text || '(missing)'}${part ? ` [${part.source}]` : ''}`);
|
|
126
|
-
}
|
|
127
|
-
lines.push('', 'Questions:', ...(result.questions ?? []).map(q => ` ${q.component}: ${q.question}`), '', `Answer: openmerit scout --conversation ${result.conversationId} --brief <component>="<answer>" ...`, `Or skip: openmerit scout --conversation ${result.conversationId} --skip`);
|
|
128
|
-
}
|
|
129
|
-
else if (result.status === 'recommended') {
|
|
130
|
-
const tasks = result.tasks ?? [];
|
|
131
|
-
tasks.forEach((task, i) => {
|
|
132
|
-
if (tasks.length > 1)
|
|
133
|
-
lines.push(`${i + 1}. ${task.title}${task.agent ? ` (${task.agent}${task.callsPerRun && task.callsPerRun > 1 ? `, ${task.callsPerRun} calls per run` : ''})` : ''}`, ` ${task.work}`);
|
|
134
|
-
// As on the page, the default cheapest-first order is unlabelled; another priority is named with the value it sorts by.
|
|
135
|
-
const priority = task.priority ?? 'cost';
|
|
136
|
-
if (priority !== 'cost')
|
|
137
|
-
lines.push(` ${rankings[priority]}`);
|
|
138
|
-
for (const m of task.models) {
|
|
139
|
-
const measuredCost = m.cost?.measured ? `measured ${money(m.cost.measured.usdPer1kAgentRuns ?? m.cost.measured.usdPer1kTasks)} per 1k ${m.cost.measured.usdPer1kAgentRuns === undefined ? 'tasks' : 'agent runs'}, ` : '';
|
|
140
|
-
const price = m.cost ? `${measuredCost}${money(m.cost.inputPerMillion)} in / ${money(m.cost.outputPerMillion)} out per 1M via ${m.cost.route} (${m.cost.endpointId})` : 'no priced route';
|
|
141
|
-
const metric = priority === 'latency' ? ` · ${m.latency ? `${m.latency.seconds.toFixed(1)}s per task` : 'time unknown'}`
|
|
142
|
-
: priority === 'recency' ? ` · ${m.recency ? `released ${m.recency.released}` : 'release date unknown'}` : '';
|
|
143
|
-
lines.push(` ${m.rank}. ${m.name}${metric} · quality ${m.quality.score}/100${m.quality.measured ? ' (measured)' : ''} · ${price}`, ` ${m.rationale}`);
|
|
144
|
-
}
|
|
145
|
-
lines.push(` ${task.summary}`);
|
|
146
|
-
if (task.evaluation)
|
|
147
|
-
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}`));
|
|
148
|
-
lines.push('');
|
|
149
|
-
});
|
|
150
|
-
if (result.assumptions?.length)
|
|
151
|
-
lines.push('Assumptions:', ...result.assumptions.map(a => ` - ${a}`), '');
|
|
152
|
-
lines.push(`Refine: openmerit scout --conversation ${result.conversationId} "<follow-up>"`);
|
|
153
|
-
lines.push(`Report measured results: openmerit scout --conversation ${result.conversationId} --results <file.json>`);
|
|
154
|
-
lines.push(tasks.some(t => t.models.some(m => m.quality.measured))
|
|
155
|
-
? 'Quality marked (measured) rests on the results you reported; the rest is Scout\'s estimate from public benchmarks.'
|
|
156
|
-
: 'Quality is Scout\'s estimate from public benchmarks, not measured accuracy for your application.');
|
|
157
|
-
}
|
|
158
|
-
else if (result.status === 'working')
|
|
159
|
-
lines.push(`Scout is still working. Read the outcome with: openmerit scout --conversation ${result.conversationId}`);
|
|
160
|
-
else
|
|
161
|
-
lines.push(result.text ?? result.error ?? 'Scout replied without an answer.');
|
|
162
|
-
return lines.join('\n');
|
|
163
|
-
}
|
|
164
|
-
export async function main(args, { root = process.cwd(), print = console.log } = {}) {
|
|
165
|
-
if (args.includes('--help') || args.includes('-h')) {
|
|
166
|
-
print(help);
|
|
167
|
-
return 0;
|
|
168
|
-
}
|
|
169
|
-
const request = {};
|
|
170
|
-
const words = [];
|
|
171
|
-
let json = false;
|
|
172
|
-
for (let i = 0; i < args.length; i++) {
|
|
173
|
-
const arg = args[i] ?? '';
|
|
174
|
-
const value = () => { const v = args[++i]; if (v === undefined || v.startsWith('--'))
|
|
175
|
-
throw new Error(`${arg} needs a value.`); return v; };
|
|
176
|
-
if (arg === '--conversation')
|
|
177
|
-
request.conversationId = value();
|
|
178
|
-
else if (arg === '--brief') {
|
|
179
|
-
const [component, ...text] = value().split('=');
|
|
180
|
-
if (!components.includes(component ?? '') || !text.length)
|
|
181
|
-
throw new Error(`--brief takes <component>=<text>; components: ${components.join(', ')}.`);
|
|
182
|
-
request.brief = { ...request.brief, [component]: text.join('=') };
|
|
183
|
-
}
|
|
184
|
-
else if (arg === '--skip')
|
|
185
|
-
request.skip = true;
|
|
186
|
-
else if (arg === '--all-endpoints')
|
|
187
|
-
request.allEndpoints = true;
|
|
188
|
-
else if (arg === '--json')
|
|
189
|
-
json = true;
|
|
190
|
-
else if (arg === '--sync') {
|
|
191
|
-
if (value() !== 'langfuse')
|
|
192
|
-
throw new Error('--sync supports langfuse.');
|
|
193
|
-
request.sync = 'langfuse';
|
|
194
|
-
}
|
|
195
|
-
else if (arg === '--results') {
|
|
196
|
-
const file = value();
|
|
197
|
-
let results;
|
|
198
|
-
try {
|
|
199
|
-
results = JSON.parse(await readFile(resolve(root, file), 'utf8'));
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
throw new Error(`Could not read results from ${file}: it must be a JSON list.`);
|
|
203
|
-
}
|
|
204
|
-
if (!Array.isArray(results))
|
|
205
|
-
throw new Error(`${file} must hold a JSON list of results.`);
|
|
206
|
-
// Scout accepts at most 50 per turn; each call is a turn that recommends again, so splitting is the caller's choice.
|
|
207
|
-
if (results.length > 50)
|
|
208
|
-
throw new Error(`${file} holds ${results.length} results; Scout accepts at most 50 per call. Split them into files of up to 50 and report each with --results in turn; Scout recommends again after each.`);
|
|
209
|
-
if (results.some(r => isObject(r) && [r.source, r.scope].some(v => typeof v === 'string' && /(?:\b(?:claim|patient|member|FICT|CLM)[-_ ]?\d|\b[0-9a-f]{8}-[0-9a-f-]{27,}\b|[^\s@]+@[^\s@]+\.[^\s@]+)/i.test(v))))
|
|
210
|
-
print('Warning: source or scope looks like a record ID or email. Send aggregate labels only; remove identifiers before sharing.');
|
|
211
|
-
request.results = results;
|
|
212
|
-
}
|
|
213
|
-
else if (arg.startsWith('-'))
|
|
214
|
-
throw new Error(`Unknown option for scout: ${arg}`);
|
|
215
|
-
else
|
|
216
|
-
words.push(arg);
|
|
217
|
-
}
|
|
218
|
-
if (words.length)
|
|
219
|
-
request.prompt = words.join(' ');
|
|
220
|
-
if (!request.prompt && !request.conversationId) {
|
|
221
|
-
print(help);
|
|
222
|
-
return 1;
|
|
223
|
-
}
|
|
224
|
-
if (request.results && !request.conversationId)
|
|
225
|
-
throw new Error('--results continues a conversation: pass --conversation <id> from the recommendation you measured.');
|
|
226
|
-
const result = await scoutTurn(root, request);
|
|
227
|
-
print(json ? JSON.stringify(result, null, 2) : renderScout(result));
|
|
228
|
-
return result.status === 'error' ? 1 : 0;
|
|
229
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export function safeFrames(error: any): {
|
|
2
|
-
filename: string | undefined;
|
|
3
|
-
lineno: number;
|
|
4
|
-
colno: number;
|
|
5
|
-
}[];
|
|
6
|
-
export function errorProperties(error: any): {
|
|
7
|
-
error_type: any;
|
|
8
|
-
frames: {
|
|
9
|
-
filename: string | undefined;
|
|
10
|
-
lineno: number;
|
|
11
|
-
colno: number;
|
|
12
|
-
}[];
|
|
13
|
-
};
|
|
14
|
-
export function setupHeaders(ids?: {}, client?: string): {
|
|
15
|
-
'X-Scout-Installation-Id'?: any;
|
|
16
|
-
'X-Scout-Setup-Id'?: any;
|
|
17
|
-
'X-Scout-Analytics-Account'?: string;
|
|
18
|
-
'X-Scout-Analytics'?: string;
|
|
19
|
-
'X-Scout-Analytics-Content': string;
|
|
20
|
-
'X-Scout-Client-Version'?: any;
|
|
21
|
-
'X-Scout-Client': string;
|
|
22
|
-
'X-Scout-Agent': string;
|
|
23
|
-
};
|
|
24
|
-
export function installationProperties(input: any): {} | null;
|
|
25
|
-
export function sendInstallationEvent(input: any): Promise<void>;
|
|
26
|
-
export function commandTelemetry(args: any): (outcome: any, error: any, code: any) => Promise<void>;
|
|
27
|
-
export let clientVersion: any;
|
|
28
|
-
export function telemetryEnabled(defaultEnabled?: boolean): boolean;
|
|
29
|
-
export function setupCorrelationEnabled(): boolean;
|
|
30
|
-
export function setupUUID(value: any): boolean;
|