openmerit 0.1.11 → 0.2.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 (72) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -56
  3. package/dist/pi/scout-extension.mjs +190 -0
  4. package/dist/pi/src/index.d.ts +6 -32
  5. package/dist/pi/src/index.js +49 -787
  6. package/dist/terminal/public/app.js +37 -37
  7. package/dist/terminal/public/styles.css +0 -125
  8. package/dist/terminal/scout-update.json +1 -1
  9. package/dist/terminal/src/cli.mjs +2 -0
  10. package/dist/terminal/src/install.mjs +103 -166
  11. package/dist/terminal/src/scout-credentials.d.ts +12 -0
  12. package/dist/terminal/src/scout-credentials.js +41 -0
  13. package/dist/terminal/src/scout-credentials.ts +44 -0
  14. package/dist/terminal/src/scout-instructions.d.ts +5 -0
  15. package/dist/terminal/src/scout-instructions.js +116 -0
  16. package/dist/terminal/src/scout-instructions.ts +119 -0
  17. package/dist/terminal/src/scout-pi.d.ts +44 -0
  18. package/dist/terminal/src/scout-pi.js +84 -0
  19. package/dist/terminal/src/scout-pi.ts +76 -0
  20. package/dist/terminal/src/scout.d.ts +77 -0
  21. package/dist/terminal/src/scout.js +181 -0
  22. package/dist/terminal/src/scout.ts +164 -0
  23. package/dist/terminal/src/server.mjs +1 -1
  24. package/dist/terminal/src/telemetry.mjs +2 -2
  25. package/docs/commands.md +3 -77
  26. package/docs/getting-started.md +20 -45
  27. package/docs/index.md +9 -14
  28. package/docs/inference-terminal.md +2 -63
  29. package/docs/pi-extension.md +32 -28
  30. package/docs/roadmap.md +6 -18
  31. package/docs/scout.md +39 -255
  32. package/docs/security.md +8 -14
  33. package/docs/testing.md +8 -34
  34. package/docs/troubleshooting.md +23 -28
  35. package/package.json +6 -27
  36. package/dist/core/src/index.d.ts +0 -101
  37. package/dist/core/src/index.js +0 -1649
  38. package/dist/core/src/store.d.ts +0 -35
  39. package/dist/core/src/store.js +0 -102
  40. package/dist/pi/src/scheduler.d.ts +0 -11
  41. package/dist/pi/src/scheduler.js +0 -137
  42. package/dist/pi/src/wakeup.d.ts +0 -2
  43. package/dist/pi/src/wakeup.js +0 -108
  44. package/dist/protocol/src/index.d.ts +0 -484
  45. package/dist/protocol/src/index.js +0 -47
  46. package/dist/protocol/src/schemas.d.ts +0 -576
  47. package/dist/protocol/src/schemas.js +0 -280
  48. package/dist/terminal/public/brands/vllm.ico +0 -0
  49. package/dist/terminal/public/flow.css +0 -1
  50. package/dist/terminal/public/flow.js +0 -770
  51. package/dist/terminal/src/demo-policy.mjs +0 -205
  52. package/dist/terminal/src/enterprise-sample.mjs +0 -151
  53. package/docs/adapter-guide.md +0 -189
  54. package/docs/architecture.md +0 -59
  55. package/docs/automation.md +0 -74
  56. package/docs/budgets.md +0 -37
  57. package/docs/demo-backfill.md +0 -31
  58. package/docs/demo-fieldkit.md +0 -49
  59. package/docs/demo-placement.md +0 -32
  60. package/docs/demo-spam.md +0 -17
  61. package/docs/demo-support.md +0 -44
  62. package/docs/demo.md +0 -74
  63. package/docs/first-trial.md +0 -60
  64. package/docs/interactive-demo.md +0 -118
  65. package/docs/lifecycle.md +0 -30
  66. package/docs/metrics-and-evidence.md +0 -48
  67. package/docs/operations.md +0 -38
  68. package/docs/pareto-spec.md +0 -76
  69. package/docs/saga/a-steadier-compass.md +0 -25
  70. package/docs/saga/defining-a-task-for-ai.md +0 -154
  71. package/docs/saga/the-benchmark-is-not-the-job.md +0 -33
  72. package/docs/ux-reference.md +0 -32
@@ -0,0 +1,181 @@
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 } from 'node:path';
7
+ import { credentialsPath, readCredentials } from "./scout-credentials.js";
8
+ export const policyFile = '.openmerit/scout.json';
9
+ export const components = ['input', 'work', 'output', 'criteria', 'constraints'];
10
+ export 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
+
15
+ Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
16
+ brief with clarifying questions: answer them with --brief (components: input,
17
+ work, output, criteria, constraints) in the same conversation, or --skip to let
18
+ Scout assume. It then splits the work into tasks and recommends up to five
19
+ models for each. A turn usually takes one to three minutes.
20
+
21
+ A new conversation is limited to the endpoint IDs in ${policyFile}
22
+ (set with openmerit project). --all-endpoints searches the whole catalogue and is
23
+ refused under the existing-only policy. Uses the device key that
24
+ "openmerit install" saved for Claude Code or Pi, this project's first. Never include secrets, source files or customer records.
25
+ `;
26
+ const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
27
+ async function readJSON(path) {
28
+ try {
29
+ const value = JSON.parse(await readFile(path, 'utf8'));
30
+ return isObject(value) ? value : null;
31
+ }
32
+ catch (error) {
33
+ if (error.code === 'ENOENT')
34
+ return null;
35
+ throw new Error(`Could not read ${path}.`);
36
+ }
37
+ }
38
+ // The device key an installation saved: this project's installation first, then the user's.
39
+ function deviceKey(root) {
40
+ for (const scope of ['project', 'user'])
41
+ for (const client of ['claude-code', 'pi']) {
42
+ const credentials = readCredentials(credentialsPath(client, scope, root));
43
+ if (credentials)
44
+ return credentials;
45
+ }
46
+ return null;
47
+ }
48
+ // One turn through the scout_models MCP tool. Errors are written for the person or agent running it.
49
+ export async function askScout(root, request, signal) {
50
+ const credentials = deviceKey(root);
51
+ if (!credentials)
52
+ throw new Error('This device is not signed in to OpenMerit. Run: npx -y openmerit@latest install');
53
+ const server = credentials.server;
54
+ let response;
55
+ try {
56
+ response = await fetch(`${server}/scout/mcp`, {
57
+ method: 'POST', redirect: 'error',
58
+ // A turn can take minutes; Scout keeps working if the connection drops.
59
+ signal: AbortSignal.any([AbortSignal.timeout(10 * 60_000), ...(signal ? [signal] : [])]),
60
+ headers: { ...credentials.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${credentials.key}`, 'X-Scout-Client': 'cli' },
61
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'scout_models', arguments: request } }),
62
+ });
63
+ }
64
+ catch (error) {
65
+ const id = request.conversationId;
66
+ 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.`);
67
+ }
68
+ if (response.status === 401)
69
+ throw new Error('The device key was rejected. Run: npx -y openmerit@latest login, then install again.');
70
+ const body = await response.json().catch(() => null);
71
+ const result = isObject(body) && isObject(body.result) ? body.result : null;
72
+ if (!result)
73
+ throw new Error(`Scout returned HTTP ${response.status}${isObject(body) && isObject(body.error) ? `: ${String(body.error.message)}` : ''}.`);
74
+ const text = Array.isArray(result.content) && isObject(result.content[0]) ? String(result.content[0].text) : '';
75
+ if (result.isError && !isObject(result.structuredContent))
76
+ throw new Error(text || 'Scout could not finish this turn.');
77
+ return result.structuredContent;
78
+ }
79
+ // A turn under the project's access policy. A new conversation is limited to the saved
80
+ // inventory; the whole catalogue needs an explicit choice the policy permits.
81
+ export async function scoutTurn(root, request, signal) {
82
+ const { allEndpoints, ...turn } = request;
83
+ if (turn.conversationId) {
84
+ if (allEndpoints)
85
+ throw new Error('--all-endpoints applies when a conversation starts. Start a new conversation instead.');
86
+ return askScout(root, turn, signal);
87
+ }
88
+ const policy = await readJSON(join(root, policyFile));
89
+ if (!policy) {
90
+ if (!allEndpoints)
91
+ 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.`);
92
+ return askScout(root, turn, signal);
93
+ }
94
+ const ids = Array.isArray(policy.endpointIds) && policy.endpointIds.every(id => typeof id === 'string') ? policy.endpointIds : null;
95
+ if (!ids || !['existing-only', 'existing-first'].includes(String(policy.policy)))
96
+ throw new Error(`${policyFile} is not a valid Scout policy. Save it again with "openmerit project".`);
97
+ if (allEndpoints && policy.policy === 'existing-only')
98
+ throw new Error('This project uses the existing-only policy, so Scout cannot search the whole catalogue.');
99
+ return askScout(root, allEndpoints ? turn : { ...turn, endpointIds: ids }, signal);
100
+ }
101
+ const money = (usd) => `$${usd < 0.1 ? usd.toFixed(3) : usd.toFixed(2)}`;
102
+ const rankings = { quality: 'Highest quality estimate first.', latency: 'Fastest first.', recency: 'Newest first.' };
103
+ export function renderScout(result) {
104
+ const lines = [];
105
+ if (result.status === 'needs_answers') {
106
+ lines.push('Scout needs more detail. Its task brief:', '');
107
+ for (const c of components) {
108
+ const part = result.brief?.[c];
109
+ lines.push(` ${c.padEnd(12)}${part?.text || '(missing)'}${part ? ` [${part.source}]` : ''}`);
110
+ }
111
+ 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`);
112
+ }
113
+ else if (result.status === 'recommended') {
114
+ const tasks = result.tasks ?? [];
115
+ tasks.forEach((task, i) => {
116
+ if (tasks.length > 1)
117
+ lines.push(`${i + 1}. ${task.title}`, ` ${task.work}`);
118
+ // As on the page, the default cheapest-first order is unlabelled; another priority is named with the value it sorts by.
119
+ const priority = task.priority ?? 'cost';
120
+ if (priority !== 'cost')
121
+ lines.push(` ${rankings[priority]}`);
122
+ for (const m of task.models) {
123
+ 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
+ const metric = priority === 'latency' ? ` · ${m.latency ? `${m.latency.seconds.toFixed(1)}s per task` : 'time unknown'}`
125
+ : 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}`);
127
+ }
128
+ lines.push(` ${task.summary}`, '');
129
+ });
130
+ if (result.assumptions?.length)
131
+ lines.push('Assumptions:', ...result.assumptions.map(a => ` - ${a}`), '');
132
+ lines.push(`Refine: openmerit scout --conversation ${result.conversationId} "<follow-up>"`);
133
+ lines.push('Quality is Scout\'s estimate from public benchmarks, not measured accuracy for your application.');
134
+ }
135
+ else if (result.status === 'working')
136
+ lines.push(`Scout is still working. Read the outcome with: openmerit scout --conversation ${result.conversationId}`);
137
+ else
138
+ lines.push(result.text ?? result.error ?? 'Scout replied without an answer.');
139
+ return lines.join('\n');
140
+ }
141
+ export async function main(args, { root = process.cwd(), print = console.log } = {}) {
142
+ if (args.includes('--help') || args.includes('-h')) {
143
+ print(help);
144
+ return 0;
145
+ }
146
+ const request = {};
147
+ const words = [];
148
+ let json = false;
149
+ for (let i = 0; i < args.length; i++) {
150
+ const arg = args[i] ?? '';
151
+ const value = () => { const v = args[++i]; if (v === undefined || v.startsWith('--'))
152
+ throw new Error(`${arg} needs a value.`); return v; };
153
+ if (arg === '--conversation')
154
+ request.conversationId = value();
155
+ else if (arg === '--brief') {
156
+ const [component, ...text] = value().split('=');
157
+ if (!components.includes(component ?? '') || !text.length)
158
+ throw new Error(`--brief takes <component>=<text>; components: ${components.join(', ')}.`);
159
+ request.brief = { ...request.brief, [component]: text.join('=') };
160
+ }
161
+ else if (arg === '--skip')
162
+ request.skip = true;
163
+ else if (arg === '--all-endpoints')
164
+ request.allEndpoints = true;
165
+ else if (arg === '--json')
166
+ json = true;
167
+ else if (arg.startsWith('-'))
168
+ throw new Error(`Unknown option for scout: ${arg}`);
169
+ else
170
+ words.push(arg);
171
+ }
172
+ if (words.length)
173
+ request.prompt = words.join(' ');
174
+ if (!request.prompt && !request.conversationId) {
175
+ print(help);
176
+ return 1;
177
+ }
178
+ const result = await scoutTurn(root, request);
179
+ print(json ? JSON.stringify(result, null, 2) : renderScout(result));
180
+ return result.status === 'error' ? 1 : 0;
181
+ }
@@ -0,0 +1,164 @@
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 } from 'node:path';
7
+ import { credentialsPath, readCredentials, type DeviceCredentials } from './scout-credentials.ts';
8
+
9
+ export const policyFile = '.openmerit/scout.json';
10
+ export const components = ['input', 'work', 'output', 'criteria', 'constraints'] as const;
11
+
12
+ export const help = `OpenMerit · Scout
13
+
14
+ openmerit scout "<the work models need to do>" [--all-endpoints] [--json]
15
+ openmerit scout --conversation <id> [--brief <component>=<text> ...] [--skip] ["<follow-up>"] [--json]
16
+
17
+ Asks Scout, the agent behind openmerit.ai/scout. Scout may first return a task
18
+ brief with clarifying questions: answer them with --brief (components: input,
19
+ work, output, criteria, constraints) in the same conversation, or --skip to let
20
+ Scout assume. It then splits the work into tasks and recommends up to five
21
+ models for each. A turn usually takes one to three minutes.
22
+
23
+ A new conversation is limited to the endpoint IDs in ${policyFile}
24
+ (set with openmerit project). --all-endpoints searches the whole catalogue and is
25
+ refused under the existing-only policy. Uses the device key that
26
+ "openmerit install" saved for Claude Code or Pi, this project's first. Never include secrets, source files or customer records.
27
+ `;
28
+
29
+ type Json = Record<string, unknown>;
30
+ export interface ScoutRequest { prompt?: string; conversationId?: string; brief?: Partial<Record<typeof components[number], string>>; skip?: boolean; endpointIds?: string[] }
31
+ export interface ScoutModel {
32
+ rank: number; id: string; name: string; provider: string; rationale: string; watchOut: string | null;
33
+ quality: { score: number; evidence: Array<{ name: string; rank: number; of: number }> };
34
+ cost: { usdPer1kTasks: number; inputPerMillion: number; outputPerMillion: number; route: string; endpointId: string; providerModelId: string } | null;
35
+ latency?: { seconds: number } | null; recency?: { released: string } | null;
36
+ }
37
+ // How a task's models are ordered. Cost (cheapest first among models that reach the quality bar) is the default.
38
+ type Priority = 'cost' | 'quality' | 'latency' | 'recency';
39
+ export interface ScoutResult {
40
+ conversationId: string; status: 'needs_answers' | 'recommended' | 'working' | 'message' | 'error'; next?: string;
41
+ brief?: Record<string, { text: string; source: string }>; questions?: Array<{ component: string; question: string }>;
42
+ assumptions?: string[]; tasks?: Array<{ title: string; work: string; summary: string; priority?: Priority; models: ScoutModel[] }>;
43
+ text?: string; error?: string;
44
+ }
45
+
46
+ const isObject = (v: unknown): v is Json => !!v && typeof v === 'object' && !Array.isArray(v);
47
+ async function readJSON(path: string): Promise<Json | null> {
48
+ try { const value: unknown = JSON.parse(await readFile(path, 'utf8')); return isObject(value) ? value : null; }
49
+ catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw new Error(`Could not read ${path}.`); }
50
+ }
51
+
52
+ // The device key an installation saved: this project's installation first, then the user's.
53
+ function deviceKey(root: string): DeviceCredentials | null {
54
+ for (const scope of ['project', 'user'] as const) for (const client of ['claude-code', 'pi'] as const) {
55
+ const credentials = readCredentials(credentialsPath(client, scope, root));
56
+ if (credentials) return credentials;
57
+ }
58
+ return null;
59
+ }
60
+
61
+ // One turn through the scout_models MCP tool. Errors are written for the person or agent running it.
62
+ export async function askScout(root: string, request: ScoutRequest, signal?: AbortSignal): Promise<ScoutResult> {
63
+ const credentials = deviceKey(root);
64
+ if (!credentials) throw new Error('This device is not signed in to OpenMerit. Run: npx -y openmerit@latest install');
65
+ const server = credentials.server;
66
+ let response: Response;
67
+ try {
68
+ response = await fetch(`${server}/scout/mcp`, {
69
+ method: 'POST', redirect: 'error',
70
+ // A turn can take minutes; Scout keeps working if the connection drops.
71
+ signal: AbortSignal.any([AbortSignal.timeout(10 * 60_000), ...(signal ? [signal] : [])]),
72
+ headers: { ...credentials.headers, 'Content-Type': 'application/json', Authorization: `Bearer ${credentials.key}`, 'X-Scout-Client': 'cli' },
73
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'scout_models', arguments: request } }),
74
+ });
75
+ } catch (error) {
76
+ const id = request.conversationId;
77
+ throw new Error((error as 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.`);
78
+ }
79
+ if (response.status === 401) throw new Error('The device key was rejected. Run: npx -y openmerit@latest login, then install again.');
80
+ const body: unknown = await response.json().catch(() => null);
81
+ const result = isObject(body) && isObject(body.result) ? body.result : null;
82
+ if (!result) throw new Error(`Scout returned HTTP ${response.status}${isObject(body) && isObject(body.error) ? `: ${String(body.error.message)}` : ''}.`);
83
+ const text = Array.isArray(result.content) && isObject(result.content[0]) ? String(result.content[0].text) : '';
84
+ if (result.isError && !isObject(result.structuredContent)) throw new Error(text || 'Scout could not finish this turn.');
85
+ return result.structuredContent as unknown as ScoutResult;
86
+ }
87
+
88
+ // A turn under the project's access policy. A new conversation is limited to the saved
89
+ // inventory; the whole catalogue needs an explicit choice the policy permits.
90
+ export async function scoutTurn(root: string, request: Omit<ScoutRequest, 'endpointIds'> & { allEndpoints?: boolean }, signal?: AbortSignal): Promise<ScoutResult> {
91
+ const { allEndpoints, ...turn } = request;
92
+ if (turn.conversationId) {
93
+ if (allEndpoints) throw new Error('--all-endpoints applies when a conversation starts. Start a new conversation instead.');
94
+ return askScout(root, turn, signal);
95
+ }
96
+ const policy = await readJSON(join(root, policyFile));
97
+ if (!policy) {
98
+ if (!allEndpoints) 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.`);
99
+ return askScout(root, turn, signal);
100
+ }
101
+ const ids = Array.isArray(policy.endpointIds) && policy.endpointIds.every(id => typeof id === 'string') ? policy.endpointIds as string[] : null;
102
+ if (!ids || !['existing-only', 'existing-first'].includes(String(policy.policy))) throw new Error(`${policyFile} is not a valid Scout policy. Save it again with "openmerit project".`);
103
+ if (allEndpoints && policy.policy === 'existing-only') throw new Error('This project uses the existing-only policy, so Scout cannot search the whole catalogue.');
104
+ return askScout(root, allEndpoints ? turn : { ...turn, endpointIds: ids }, signal);
105
+ }
106
+
107
+ const money = (usd: number) => `$${usd < 0.1 ? usd.toFixed(3) : usd.toFixed(2)}`;
108
+ const rankings: Record<Exclude<Priority, 'cost'>, string> = { quality: 'Highest quality estimate first.', latency: 'Fastest first.', recency: 'Newest first.' };
109
+ export function renderScout(result: ScoutResult): string {
110
+ const lines: string[] = [];
111
+ if (result.status === 'needs_answers') {
112
+ lines.push('Scout needs more detail. Its task brief:', '');
113
+ for (const c of components) { const part = result.brief?.[c]; lines.push(` ${c.padEnd(12)}${part?.text || '(missing)'}${part ? ` [${part.source}]` : ''}`); }
114
+ lines.push('', 'Questions:', ...(result.questions ?? []).map(q => ` ${q.component}: ${q.question}`), '',
115
+ `Answer: openmerit scout --conversation ${result.conversationId} --brief <component>="<answer>" ...`,
116
+ `Or skip: openmerit scout --conversation ${result.conversationId} --skip`);
117
+ } else if (result.status === 'recommended') {
118
+ const tasks = result.tasks ?? [];
119
+ tasks.forEach((task, i) => {
120
+ if (tasks.length > 1) lines.push(`${i + 1}. ${task.title}`, ` ${task.work}`);
121
+ // As on the page, the default cheapest-first order is unlabelled; another priority is named with the value it sorts by.
122
+ const priority = task.priority ?? 'cost';
123
+ if (priority !== 'cost') lines.push(` ${rankings[priority]}`);
124
+ for (const m of task.models) {
125
+ 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';
126
+ const metric = priority === 'latency' ? ` · ${m.latency ? `${m.latency.seconds.toFixed(1)}s per task` : 'time unknown'}`
127
+ : priority === 'recency' ? ` · ${m.recency ? `released ${m.recency.released}` : 'release date unknown'}` : '';
128
+ lines.push(` ${m.rank}. ${m.name}${metric} · quality ${m.quality.score}/100 · ${price}`, ` ${m.rationale}`);
129
+ }
130
+ lines.push(` ${task.summary}`, '');
131
+ });
132
+ if (result.assumptions?.length) lines.push('Assumptions:', ...result.assumptions.map(a => ` - ${a}`), '');
133
+ lines.push(`Refine: openmerit scout --conversation ${result.conversationId} "<follow-up>"`);
134
+ lines.push('Quality is Scout\'s estimate from public benchmarks, not measured accuracy for your application.');
135
+ } else if (result.status === 'working') lines.push(`Scout is still working. Read the outcome with: openmerit scout --conversation ${result.conversationId}`);
136
+ else lines.push(result.text ?? result.error ?? 'Scout replied without an answer.');
137
+ return lines.join('\n');
138
+ }
139
+
140
+ export async function main(args: string[], { root = process.cwd(), print = console.log } = {}): Promise<number> {
141
+ if (args.includes('--help') || args.includes('-h')) { print(help); return 0; }
142
+ const request: Parameters<typeof scoutTurn>[1] = {};
143
+ const words: string[] = [];
144
+ let json = false;
145
+ for (let i = 0; i < args.length; i++) {
146
+ const arg = args[i] ?? '';
147
+ const value = () => { const v = args[++i]; if (v === undefined || v.startsWith('--')) throw new Error(`${arg} needs a value.`); return v; };
148
+ if (arg === '--conversation') request.conversationId = value();
149
+ else if (arg === '--brief') {
150
+ const [component, ...text] = value().split('=');
151
+ if (!(components as readonly string[]).includes(component ?? '') || !text.length) throw new Error(`--brief takes <component>=<text>; components: ${components.join(', ')}.`);
152
+ request.brief = { ...request.brief, [component as typeof components[number]]: text.join('=') };
153
+ } else if (arg === '--skip') request.skip = true;
154
+ else if (arg === '--all-endpoints') request.allEndpoints = true;
155
+ else if (arg === '--json') json = true;
156
+ else if (arg.startsWith('-')) throw new Error(`Unknown option for scout: ${arg}`);
157
+ else words.push(arg);
158
+ }
159
+ if (words.length) request.prompt = words.join(' ');
160
+ if (!request.prompt && !request.conversationId) { print(help); return 1; }
161
+ const result = await scoutTurn(root, request);
162
+ print(json ? JSON.stringify(result, null, 2) : renderScout(result));
163
+ return result.status === 'error' ? 1 : 0;
164
+ }
@@ -6,7 +6,7 @@ import { createCollector, loadConfig } from './collect.mjs';
6
6
  import { sampleSnapshot } from './sample.mjs';
7
7
  import { telemetryEnabled, terminalProperties, sendTerminalEvent, errorProperties } from './telemetry.mjs';
8
8
 
9
- const assets = new Map([['/', ['index.html', 'text/html']], ['/app.js', ['app.js', 'text/javascript']], ['/flow.js', ['flow.js', 'text/javascript']], ['/flow.css', ['flow.css', 'text/css']], ['/styles.css', ['styles.css', 'text/css']], ['/favicon.svg', ['favicon.svg', 'image/svg+xml']]]);
9
+ const assets = new Map([['/', ['index.html', 'text/html']], ['/app.js', ['app.js', 'text/javascript']], ['/styles.css', ['styles.css', 'text/css']], ['/favicon.svg', ['favicon.svg', 'image/svg+xml']]]);
10
10
  for (const file of ['baseten.png', 'groq.ico', 'cerebras.png', 'qwen.png', 'meta.ico', 'nomic.png', 'baai.png', 'ollama.png', 'lm-studio.png', 'vllm.png', 'anthropic.png', 'google.png', 'deepseek.ico', 'cohere.png', 'openai.png', 'mistral.png', 'openrouter.png', 'huggingface.svg']) assets.set(`/brands/${file}`, [`brands/${file}`, file.endsWith('.svg') ? 'image/svg+xml' : file.endsWith('.ico') ? 'image/x-icon' : 'image/png']);
11
11
 
12
12
  export async function startTerminal({ root = process.cwd(), port = 4310, interval = 15, demo = false, collect: customCollect, config, fallbackPort = false } = {}) {
@@ -38,7 +38,7 @@ export const setupUUID = value => typeof value === 'string' && /^[0-9a-f]{8}-[0-
38
38
  export function setupHeaders(ids = {}, client = 'cli') {
39
39
  return {
40
40
  'X-Scout-Client': client,
41
- 'X-Scout-Agent': 'claude-code',
41
+ 'X-Scout-Agent': ids.agent === 'pi' ? 'pi' : 'claude-code',
42
42
  ...(clientVersion ? { 'X-Scout-Client-Version': clientVersion } : {}),
43
43
  'X-Scout-Analytics-Content': 'off',
44
44
  ...(client === 'cli' ? { 'X-Scout-Analytics': telemetryEnabled(true) ? 'on' : 'off' } : !telemetryEnabled(true) ? { 'X-Scout-Analytics': 'off' } : {}),
@@ -54,7 +54,7 @@ export function installationProperties(input) {
54
54
  stage: ['started', 'preflight', 'login', 'browser_open', 'configuration', 'verification', 'status', 'remove', 'completed'],
55
55
  outcome: ['started', 'ok', 'error', 'pending', 'denied', 'conflict', 'not_installed'],
56
56
  command: ['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor', 'unknown'],
57
- agent: ['claude-code'], scope: ['user', 'project'],
57
+ agent: ['claude-code', 'pi'], scope: ['user', 'project'],
58
58
  rule_status: ['matches', 'missing', 'modified_or_unowned'],
59
59
  skill_status: ['matches', 'missing', 'modified_or_unowned'],
60
60
  server_status: ['matches', 'missing', 'modified_or_unowned'],
package/docs/commands.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Commands
2
2
 
3
- The standalone `openmerit dash` command runs in your shell. The `/openmerit` slash commands run inside Pi with the extension loaded.
3
+ **Pi adapter — npm 0.1.12 and later:** Pi 1.5.0 uses only native Scout MCP and shared guidance. Run `npx -y openmerit@latest install --client pi [--scope user|project]` and approve in the browser. Shell `login`, `logout`, `status`, `remove`, and `doctor` accept the same target flags. Updates require explicit reinstall; no Pi update hooks or scheduler are installed. See [Pi setup](pi-extension.md).
4
+
5
+ The standalone `openmerit dash` and `openmerit eval` commands are unchanged. The Pi 0.87 `/openmerit` slash commands were removed with that adapter and have no replacement.
4
6
 
5
7
  Stable npm `openmerit@0.1.7` provides `openmerit eval init`, `doctor`, `plan`, `run`, `watch` and `report`. See [agent evaluations](agent-evaluations.md) for scope selection, repeated task checks and budgets. These are shell commands, separate from Pi commands. The same release includes [Scout setup commands](scout.md#commands), beginning with `npx -y openmerit@latest install --client claude-code`.
6
8
 
@@ -11,79 +13,3 @@ Since `0.1.7-preview.2`, `openmerit eval plan [--scope existing|all] [--budget <
11
13
  Install with `npm install -g openmerit` to use `openmerit dash [path]` for a passive local view of models, connections, usage, and compute. From this checkout, use `npm run dash -- [path]`. It is independent of Pi setup and does not run model comparisons or inference.
12
14
 
13
15
  See the [Inference Terminal guide](inference-terminal.md) for local installation, automatic discovery, `--no-discovery`, `--json`, `--demo`, collection intervals, and data limitations. Discovery is part of `dash` and is scoped to application evidence. Coding-harness histories are not collected.
14
-
15
- ## Inspect status
16
-
17
- ```text
18
- /openmerit
19
- /openmerit status
20
- ```
21
-
22
- Both show the task profile, metric readiness, evaluation status, active work, next assessment, lifecycle stage, incumbent candidate, candidate and assessment counts, challenger spend, proposed candidate, and pause flag. A readiness label is based on the harness’s structured reports; inspect the evidence for the underlying measurements.
23
-
24
- ## Start or recover setup
25
-
26
- ```text
27
- /openmerit setup
28
- ```
29
-
30
- Requests setup explicitly or reopens setup to correct stale target/incumbent state. Accepted replacement setup clears prior candidate, assessment, frontier, and baseline handoffs while retaining the audit log.
31
-
32
- Request setup explicitly only when recovering, rerunning, or configuring a custom integration. An unconfigured project starts setup when bounded source detection finds an application LLM call at session start or after a settled Pi build turn. OpenMerit refuses another intent while work is active.
33
-
34
- ## Cancel interrupted work
35
-
36
- ```text
37
- /openmerit cancel
38
- ```
39
-
40
- Record the active intent as cancelled after an interrupted or abandoned harness run. This clears durable active-work state without claiming success and releases any interactive request that was queued behind setup.
41
-
42
- ## Request an assessment
43
-
44
- ```text
45
- /openmerit assess
46
- ```
47
-
48
- Ask OpenMerit core to check stored baseline metric windows now. It reports missing or immature required metrics without starting Pi, or starts candidate discovery when they pass. It does not create missing measurements or override sample requirements.
49
-
50
- ## Request a frontier
51
-
52
- ```text
53
- /openmerit frontier
54
- ```
55
-
56
- Ask the harness to calculate a model comparison now. OpenMerit validates the returned frontier against the confirmed profile and supplied assessments. The command does not itself authorize a model change.
57
-
58
- ## Approve a proposal
59
-
60
- ```text
61
- /openmerit approve
62
- ```
63
-
64
- Approve the current proposal after its frontier result has passed validation. If there is no proposal in the expected lifecycle stage, the command reports that no verified proposal is awaiting approval. See [comparisons and swaps](lifecycle.md) for automatic-swap policy.
65
-
66
- ## Pause and resume
67
-
68
- ```text
69
- /openmerit pause
70
- /openmerit resume
71
- ```
72
-
73
- Pause is persisted in `.openmerit/state.json`, blocks automatic due decisions and lifecycle follow-ups, and unloads the project's managed macOS LaunchAgent. Incoming signals can still be recorded. Resume clears pause and restores scheduling only when the confirmed policy authorizes it. Neither command cancels an already-active intent; explicit commands remain available. The standalone Inference Terminal is independent and keeps collecting until its CLI process exits.
74
-
75
- ## Locate logs and evidence
76
-
77
- ```text
78
- /openmerit logs
79
- ```
80
-
81
- Show the local audit, exporter-error, evidence-manifest, and state paths. See [project files and logs](operations.md) for their contents.
82
-
83
- ## Check the installation
84
-
85
- ```text
86
- /openmerit doctor
87
- ```
88
-
89
- Show adapter diagnostics, UI availability, and scheduling support. This checks extension wiring; it does not prove provider authentication, successful evaluations, or a completed model swap.
@@ -1,6 +1,6 @@
1
1
  # Getting started
2
2
 
3
- Stable npm 0.1.7 includes a separate [agent-evaluation CLI-change loop](agent-evaluations.md) for Claude Code, Codex and OpenCode, plus [Scout device setup for Claude Code](scout.md#connect-a-coding-agent). Neither is part of the Pi setup below.
3
+ OpenMerit provides a passive Inference Terminal, Scout model-selection guidance, and a separate [agent-evaluation CLI](agent-evaluations.md). Pi Scout setup requires OpenMerit **0.1.12** or newer and Pi **1.5.0**. Package publication and website deployment are independent releases.
4
4
 
5
5
  ## Open your inference environment
6
6
 
@@ -12,65 +12,40 @@ cd /path/to/your/application
12
12
  openmerit dash
13
13
  ```
14
14
 
15
- The Inference Terminal is included in stable `openmerit@0.1.7`. It opens a local, read-only view of the selected application's services, models, recorded requests, and linked compute. It does not require Pi or a provider API key, make inference calls, or change model assignments. Supported code/configuration declarations identify inventory; request history requires existing supported logs or exports. See [the terminal guide](inference-terminal.md) for collection, privacy, configuration, and diagnostics.
15
+ The Inference Terminal opens a local, read-only view of the selected application's services, models, recorded requests, and linked compute. It does not require Pi or a provider API key, make inference calls, or change model assignments. Supported code/configuration declarations identify inventory; request history requires existing supported logs or exports. See [the terminal guide](inference-terminal.md) for collection, privacy, configuration, and diagnostics.
16
16
 
17
- To inspect fictional data without installation, open the [hosted dashboard](https://dash.openmerit.ai/) or its [before/after comparison](https://dash.openmerit.ai/demo). Neither connects to your machine.
17
+ To inspect fictional data, run `openmerit dash --demo` or choose the sample from the environment selector. Sample data never enters the real snapshot or project files.
18
18
 
19
- ## Set up model comparisons with Pi
19
+ ## Connect Scout to a coding agent
20
20
 
21
- The Pi integration is a separate workflow for evaluations and authorized model changes. It requires Pi 0.87 with a model provider configured, and a product repository where Pi can create and verify evaluation and observability artifacts. Provider credentials belong to Pi or its environment; OpenMerit does not accept them.
22
-
23
- Install the extension in Pi:
24
-
25
- ```sh
26
- pi install npm:openmerit
27
- pi list
28
- ```
29
-
30
- Run `/openmerit doctor` inside Pi to confirm that the extension is available.
31
-
32
- ## Install from source
21
+ Claude Code setup is described in [the Scout guide](scout.md#connect-a-coding-agent). For the new Pi integration, use Pi **1.5.0**, Node.js 22.19 or newer, and npm. Run the intended command in your shell:
33
22
 
34
23
  ```sh
35
- git clone https://github.com/laz-aslam/openmerit.git
36
- cd openmerit
37
- npm ci
24
+ npx -y openmerit@latest install --client pi --scope user
38
25
  ```
39
26
 
40
- Start Pi with the extension:
27
+ Approve the matching device code in the browser, then start a **new Pi session**. Only approve a request you initiated. User scope is the default and applies across projects. For one project, run this from that project's root instead:
41
28
 
42
29
  ```sh
43
- ./node_modules/.bin/pi --no-extensions -e ./packages/pi/src/index.ts
30
+ npx -y openmerit@latest install --client pi --scope project
44
31
  ```
45
32
 
46
- `--no-extensions` prevents a globally installed npm copy from conflicting with the source extension. Run `/openmerit doctor` inside Pi to confirm that the adapter and intent executor are available.
47
-
48
- ## Establish a product profile
49
-
50
- Open the product repository in interactive Pi with the extension loaded. If the project has no OpenMerit state, the Pi adapter performs a bounded source preflight. It also scans after a settled Pi build turn, so an application created from an initially empty directory can enter setup without a slash command. A repository without such a target remains idle; use `/openmerit setup` only to recover or configure a custom integration explicitly.
33
+ Each client/scope installation has its own private Scout key; project keys are stored outside the repository. Do not install overlapping Pi user/project adapters or a duplicate `openmerit` MCP entry. The installer reports conflicts rather than overwriting edited or unowned files.
51
34
 
52
- OpenMerit sends Pi a private, typed `establish_evals` intent. Pi must identify the application LLM call or shared route under evaluation and its exact incumbent application model. It inspects the application's outputs, tools, and failure modes to infer task-relevant metrics instead of asking you to invent them. Before running setup, Pi shows the proposed metrics, why each matters, the representative case and repetition counts, aggregation, any actual performance threshold, and estimated evaluation cost. You confirm or edit that plan together with the stable target ID, call sites, route key, budget, check cadence, scheduling mode, and automation permissions. Pi's own model is never an application target.
35
+ The adapter connects Scout through Pi's native MCP and supplies the shared Scout rule and skill. Pi owns tool discovery, tool execution, model choice, permissions, and sessions. Scout recommends application-model candidates; it does not execute those candidates or change Pi's own model. There are no `/openmerit` lifecycle commands, automatic evaluation setup, scheduler, or LaunchAgent in this adapter.
53
36
 
54
- For an active-session demo, Pi defaults the baseline and post-swap verification cadence to one completed evaluation task unless you change it. OpenMerit rejects empty baseline or post-swap cadences.
37
+ ## Check, update, or remove Pi setup
55
38
 
56
- Pi is also asked to research preliminary model leads during setup. Each reported lead has a current price estimate and reputation summary with sources and check times. OpenMerit stores these leads with the project profile. The field is optional for compatibility with existing protocol-0.1 harnesses, and an empty list means no credible leads were found. It does not change the application model or count as a measurement.
57
-
58
- OpenMerit rejects successful setup without a confirmed task profile, at least one valid objective with an explicit sampling plan, a supervised-graduation policy, non-empty baseline and post-swap cadences, a budget, and evidence.
59
-
60
- The setup instruction asks Pi to run the application path with working provider authentication, execute each required collector and grader, inspect the resulting metric values, and retain verification artifacts. A metric that cannot be produced must be marked missing. This smoke run verifies the pipeline; it is not counted as a production baseline sample. OpenMerit still checks the reported coverage and evidence references, not the authenticity of a grader run itself.
61
-
62
- ## Collect the baseline
63
-
64
- After setup, instrument the confirmed application target and use the product normally. Report application-task completions and cumulative metric windows through Pi's typed signal tool, including the application model where applicable, sample counts, and durable evidence. Pi's own model, tokens, cost, and latency are orchestration telemetry and do not satisfy product metrics. OpenMerit core checks the stored baseline windows at the approved cadence without calling Pi; missing evidence stays insufficient.
65
-
66
- ## Find the frontier
67
-
68
- When the baseline meets every required metric threshold, OpenMerit issues the next approved nudges: discover candidates, run controlled challenger trials, and calculate the Pareto frontier. Pi performs that work. OpenMerit checks the returned result against the [Pareto specification](pareto-spec.md).
39
+ ```sh
40
+ npx -y openmerit@latest status --client pi --scope user --check
41
+ npx -y openmerit@latest doctor --client pi --scope user
42
+ npx -y openmerit@latest remove --client pi --scope user
43
+ ```
69
44
 
70
- Candidate discovery uses current price, capability, availability, reputation, and benchmark signals to shortlist models worth testing. These are starting clues, not evidence that a model performs well on your application. OpenMerit collects preliminary leads at setup, then refreshes them, probes availability, and freezes the incumbent plus challengers. Pi must use the same cases, application revision, prompt, tools, grader, and run count, with a recorded shuffle seed. It preserves one experiment manifest and reports measured quality, cost, latency, tokens, and other required metrics. OpenMerit durably carries the accepted candidate set and assessments into later intents rather than relying on Pi conversation history. A model proposal must come from the verified frontier of those measured candidate results, not from price or reputation alone.
45
+ Use `--scope project` from the original project root for a project installation. `login` and `logout` accept the same `--client` and `--scope` flags. These commands target that installation, not another client's connection. Updating Pi setup requires explicitly rerunning `install` and starting a new session; no Pi auto-update hooks are installed. Installing or updating the npm package alone does not reconfigure Pi.
71
46
 
72
- ## Approve an early swap
47
+ See [the Pi extension guide](pi-extension.md) for installed paths, migration from Pi 0.87, and SDK embedding. Use [troubleshooting](troubleshooting.md) for setup failures.
73
48
 
74
- With automatic swaps disabled, a model proposal that passes frontier validation waits for `/openmerit approve`. That authorizes Pi to apply only the proposed change. OpenMerit then requires post-swap evaluation; the approved policy determines rollback behavior. Automatic swaps may bypass per-proposal approval when enabled and the configured verified-swap threshold is reached, including when that threshold is zero.
49
+ ## Evaluate application changes
75
50
 
76
- Continue with [your first supervised trial](first-trial.md) for a practical walkthrough.
51
+ The standalone `openmerit eval` CLI is unchanged by the new Pi adapter. It evaluates Claude Code, Codex and OpenCode through explicit task, scope, budget and grader configuration; see [agent evaluations](agent-evaluations.md).
package/docs/index.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenMerit documentation
2
2
 
3
- These guides describe the implementation on **main**. Stable npm `openmerit@0.1.7` includes the Inference Terminal, agent-evaluation CLI and Scout device setup for Claude Code. The website, hosted samples, Scout service and npm package are deployed independently; dated receipts describe the versions actually tested.
3
+ These guides distinguish current source work from published behavior. OpenMerit **0.1.12** adds native Scout support for Pi **1.5.0**, replacing the old Pi 0.87 evaluation extension. The Inference Terminal and standalone agent-evaluation CLI are separate products and are unchanged by that replacement. The website, Scout service and npm package are deployed independently; dated receipts describe the versions actually tested.
4
4
 
5
5
  ## See your inference environment
6
6
 
@@ -14,27 +14,22 @@ openmerit dash
14
14
 
15
15
  The terminal shows application services, model declarations, recorded requests, and linked compute. It reads supported local evidence, needs no Pi installation or provider API key, and makes no inference calls or model changes. Configuration can identify declared services; actual activity requires existing supported logs or exports.
16
16
 
17
- Start with [installation and setup](getting-started.md) or the [Inference Terminal guide](inference-terminal.md). Explore the [hosted sample](https://dash.openmerit.ai/) and [before/after dashboard demo](https://dash.openmerit.ai/demo) without connecting your machine. The [demo guide](demo.md) explains their fictional data and comparison limits.
17
+ Start with [installation and setup](getting-started.md) or the [Inference Terminal guide](inference-terminal.md).
18
18
 
19
19
  ## Discover model candidates
20
20
 
21
21
  For a shortlist before building an integration, the separate [Scout beta](scout.md) accepts a task description and returns up to five evidence-backed model candidates. It is available at [openmerit.ai/scout/](https://openmerit.ai/scout/), with a prompt-only JSON API and MCP tool for coding agents.
22
22
 
23
- ## Evaluate model changes with Pi
23
+ ## Connect Scout to Pi
24
24
 
25
- The separate Pi 0.87 integration asks a coding harness to establish evaluations, gather application measurements, and test candidates. OpenMerit coordinates the approved lifecycle, records evidence references, validates structured results and frontier conformance, and requests authorized changes. The harness executes the work; the terminal does not perform this workflow.
25
+ The thin Pi 1.5.0 adapter uses native MCP and shared Scout guidance. Pi owns tools, models, permissions and sessions; the adapter has no `/openmerit` lifecycle commands, evaluation scheduling, LaunchAgent or automatic updates.
26
26
 
27
- Follow [Pi setup](getting-started.md#set-up-model-comparisons-with-pi), then [your first supervised trial](first-trial.md).
27
+ Follow [setup](getting-started.md#connect-scout-to-a-coding-agent) and [the Pi guide](pi-extension.md), including browser approval, per-client/scope credentials, explicit reinstall updates and SDK requirements. Registry setup requires OpenMerit 0.1.12 or newer.
28
28
 
29
- - [Metrics and evidence](metrics-and-evidence.md): measurements, sample requirements, and readiness.
30
- - [Comparisons and swaps](lifecycle.md): baseline, challenger trials, approval, verification, and rollback.
31
- - [Budgets and permissions](budgets.md): challenger accounting and provider-spend limits.
32
- - [Automatic checks](automation.md): durable signals, cadence, and authorized macOS scheduling.
29
+ ## Evaluate application changes
33
30
 
34
- ## Operate and troubleshoot
35
-
36
- [Commands](commands.md) and [project files and logs](operations.md) explain status, pause, diagnostics, and recovery. [Troubleshooting](troubleshooting.md), [security and data](security.md), and [current limitations](roadmap.md) describe the boundaries to check before relying on automation.
31
+ Use [the standalone evaluation CLI](agent-evaluations.md) to compare CLI revisions across Claude Code, Codex and OpenCode with explicit tasks, model scope, budgets and graders.
37
32
 
38
- ## Build an integration
33
+ ## Operate and troubleshoot
39
34
 
40
- Pi is the implemented coding-harness adapter. Other harnesses can implement the protocol and coordinator boundary described in [architecture](architecture.md) and [the adapter guide](adapter-guide.md). The [Pareto specification](pareto-spec.md) defines comparison rules; [testing and validation](testing.md) distinguishes automated coverage from product evidence.
35
+ [Commands](commands.md) lists the terminal, Scout setup and evaluation commands. [Troubleshooting](troubleshooting.md), [security and data](security.md), and [current limitations](roadmap.md) explain setup failures and product boundaries. [Testing and validation](testing.md) distinguishes automated coverage from product evidence.