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,138 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { readFileSync } from 'node:fs';
3
-
4
- export let clientVersion;
5
- for (const path of ['../../../package.json', '../../../../package.json']) {
6
- try { const pkg = JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')); if (pkg.name === 'openmerit') { clientVersion = pkg.version; break; } } catch {}
7
- }
8
- const frameFiles = Object.freeze(['cli.mjs', 'install.mjs']);
9
- const applicationURLs = frameFiles.map(file => new URL(`../../terminal/src/${file}`, import.meta.url).href);
10
- export function safeFrames(error) {
11
- try {
12
- const stack = error?.stack;
13
- if (typeof stack !== 'string') return [];
14
- return stack.slice(0, 8000).split('\n').slice(1, 40).flatMap(line => {
15
- const match = line.match(/(?:\(|\s)(file:\/\/[^\s)]+):(\d+):(\d+)\)?$/);
16
- if (!match || !applicationURLs.includes(match[1])) return [];
17
- const lineNumber = Number(match[2]), column = Number(match[3]);
18
- return lineNumber > 0 && lineNumber <= 1000000 && column > 0 && column <= 1000000 ? [{ filename: match[1].split('/').at(-1), lineno: lineNumber, colno: column }] : [];
19
- }).slice(0, 8);
20
- } catch { return []; }
21
- }
22
- export function errorProperties(error) {
23
- try { return { error_type: errorType(error), frames: safeFrames(error) }; }
24
- catch { return { error_type: 'Error', frames: [] }; }
25
- }
26
- const environment = () => ({ ...(clientVersion ? { client_version: clientVersion } : {}), platform: ['darwin', 'linux', 'win32'].includes(process.platform) ? process.platform : 'unknown', runtime_major: Number(process.versions.node.split('.')[0]) });
27
-
28
- const anonymousId = randomUUID();
29
- export const telemetryEnabled = (defaultEnabled = false) => (process.env.OPENMERIT_TELEMETRY ? process.env.OPENMERIT_TELEMETRY === '1' : defaultEnabled) && process.env.DO_NOT_TRACK !== '1';
30
- export const setupCorrelationEnabled = () => telemetryEnabled(true) && process.env.OPENMERIT_ANALYTICS_ACCOUNT !== 'off';
31
- export const setupUUID = value => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
32
- // Scout setup defaults to analytics on; explicit privacy refusals must survive
33
- // device approval and future agent sessions. Prompt content stays off.
34
- export function setupHeaders(ids = {}, client = 'cli') {
35
- return {
36
- 'X-Scout-Client': client,
37
- 'X-Scout-Agent': ids.agent === 'pi' ? 'pi' : 'claude-code',
38
- ...(clientVersion ? { 'X-Scout-Client-Version': clientVersion } : {}),
39
- 'X-Scout-Analytics-Content': 'off',
40
- ...(client === 'cli' ? { 'X-Scout-Analytics': telemetryEnabled(true) ? 'on' : 'off' } : !telemetryEnabled(true) ? { 'X-Scout-Analytics': 'off' } : {}),
41
- ...(process.env.OPENMERIT_ANALYTICS_ACCOUNT === 'off' || !telemetryEnabled(true) ? { 'X-Scout-Analytics-Account': 'off' } : {}),
42
- ...(setupCorrelationEnabled() && setupUUID(ids.setup_id) ? { 'X-Scout-Setup-Id': ids.setup_id } : {}),
43
- ...(setupCorrelationEnabled() && setupUUID(ids.installation_id) ? { 'X-Scout-Installation-Id': ids.installation_id } : {}),
44
- };
45
- }
46
- export function installationProperties(input) {
47
- if (!input || typeof input !== 'object' || Array.isArray(input)) return null;
48
- const out = {};
49
- const enums = {
50
- stage: ['started', 'preflight', 'login', 'browser_open', 'configuration', 'verification', 'status', 'remove', 'completed'],
51
- outcome: ['started', 'ok', 'error', 'pending', 'denied', 'conflict', 'not_installed'],
52
- command: ['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor', 'unknown'],
53
- agent: ['claude-code', 'pi'], scope: ['user', 'project'],
54
- rule_status: ['matches', 'missing', 'modified_or_unowned'],
55
- skill_status: ['matches', 'missing', 'modified_or_unowned'],
56
- server_status: ['matches', 'missing', 'modified_or_unowned'],
57
- connection: ['connected', 'failed', 'key_rejected', 'not_checked'],
58
- code: ['command_error', 'approval_pending', 'access_denied', 'install_conflict', 'openmerit_unreachable'],
59
- };
60
- for (const [key, values] of Object.entries(enums)) {
61
- if (input[key] === undefined && !['stage', 'outcome'].includes(key)) continue;
62
- if (!values.includes(input[key])) return null;
63
- out[key] = input[key];
64
- }
65
- for (const key of ['setup_id', 'installation_id']) if (setupUUID(input[key])) out[key] = input[key];
66
- for (const key of ['applied', 'installed']) if (typeof input[key] === 'boolean') out[key] = input[key];
67
- return out;
68
- }
69
- export async function sendInstallationEvent(input) {
70
- if (!telemetryEnabled(true)) return;
71
- const properties = installationProperties(input);
72
- if (!properties) return;
73
- if (!setupCorrelationEnabled()) { delete properties.setup_id; delete properties.installation_id; }
74
- // One immutable envelope across a transient retry. A hard total deadline
75
- // bounds awaited milestones even if the transport ignores cancellation.
76
- const body = JSON.stringify({ event: 'scout_installation_step', event_id: randomUUID(), anonymousId, properties: { ...properties, ...environment() } });
77
- const controller = new AbortController();
78
- let timer;
79
- try {
80
- await Promise.race([
81
- (async () => {
82
- for (let attempt = 0; attempt < 2 && !controller.signal.aborted && telemetryEnabled(true); attempt++) {
83
- try {
84
- const response = await fetch('https://openmerit.ai/scout/api/analytics', {
85
- method: 'POST', headers: { 'Content-Type': 'application/json', ...setupHeaders(properties) },
86
- body, signal: controller.signal, redirect: 'error',
87
- });
88
- await response.body?.cancel();
89
- if (response.ok || response.status !== 429 && response.status < 500) return;
90
- } catch { /* Only a bounded retry; never export error text. */ }
91
- }
92
- })(),
93
- new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(); }, 1500); }),
94
- ]);
95
- } catch { /* Never affect setup, even if the transport ignores abort. */ }
96
- finally { clearTimeout(timer); }
97
- }
98
- const errorType = error => ['Error', 'TypeError', 'RangeError', 'SyntaxError', 'ReferenceError', 'URIError', 'EvalError', 'AggregateError', 'AbortError'].includes(error?.name) ? error.name : 'Error';
99
- // Command identity is ephemeral; the installer separately manages the owned
100
- // installation's correlation IDs.
101
- export function commandTelemetry(args) {
102
- const setup = ['install', 'status', 'remove', 'login', 'logout', 'project', 'doctor'];
103
- const enabled = () => telemetryEnabled(setup.includes(args[0]));
104
- if (!enabled()) return async () => {};
105
- const started = performance.now();
106
- const command_id = randomUUID();
107
- const command = !args.length || args.includes('--help') || args.includes('-h')
108
- ? 'help' : setup.includes(args[0]) ? args[0] : 'unknown';
109
- async function send(event, properties) {
110
- if (!enabled()) return;
111
- const controller = new AbortController();
112
- const body = JSON.stringify({ event, event_id: randomUUID(), anonymousId, properties: { ...properties, ...environment(), command_id } });
113
- let timer;
114
- try {
115
- await Promise.race([
116
- Promise.resolve().then(() => fetch('https://openmerit.ai/scout/api/analytics', {
117
- method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Scout-Client': 'cli', 'X-Scout-Analytics': 'on', ...(process.env.OPENMERIT_ANALYTICS_ACCOUNT === 'off' ? { 'X-Scout-Analytics-Account': 'off' } : {}) },
118
- body,
119
- signal: controller.signal, redirect: 'error',
120
- })).then(response => response.body?.cancel()),
121
- new Promise(resolve => { timer = setTimeout(() => { controller.abort(); resolve(); }, 750); }),
122
- ]);
123
- } catch { /* Analytics must never affect command behavior. */ }
124
- finally { clearTimeout(timer); }
125
- }
126
- const pending = send('cli_command_started', { command });
127
- let completed = false;
128
- // code: a content-free setup outcome such as approval_pending; never a message.
129
- return async (outcome, error, code) => {
130
- if (completed) return;
131
- completed = true;
132
- await Promise.all([pending, send('cli_command_completed', {
133
- command, outcome: outcome === 'ok' ? 'ok' : 'error',
134
- ...(outcome === 'ok' ? {} : { code: code ?? 'command_error', ...(error ? errorProperties(error) : {}) }),
135
- duration_ms: Math.min(86400000, Math.max(0, Math.round(performance.now() - started))),
136
- })]);
137
- };
138
- }
@@ -1,397 +0,0 @@
1
- // Standalone updater copied into Claude's configuration directory. Only Node
2
- // built-ins: it must survive the npx cache disappearing, without installing or
3
- // executing code from the release it downloads.
4
- import { createHash, randomUUID } from 'node:crypto';
5
- import { realpathSync } from 'node:fs';
6
- import { mkdir, open, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
7
- import { dirname, join, resolve } from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { gunzipSync } from 'node:zlib';
10
- export const updateContract = 'scout-recommend-v2';
11
- const registry = 'https://registry.npmjs.org/openmerit/latest';
12
- const day = 24 * 60 * 60 * 1000;
13
- const retry = 15 * 60 * 1000;
14
- const digest = (text) => createHash('sha256').update(text).digest('hex');
15
- const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
16
- const parsed = (text) => {
17
- const value = JSON.parse(text);
18
- if (!isObject(value))
19
- throw new Error('Expected a JSON object.');
20
- return value;
21
- };
22
- async function optional(path) {
23
- try {
24
- return await readFile(path, 'utf8');
25
- }
26
- catch (error) {
27
- if (isObject(error) && error.code === 'ENOENT')
28
- return null;
29
- throw error;
30
- }
31
- }
32
- async function object(path) { return parsed(await optional(path) ?? '{}'); }
33
- async function atomic(path, value) {
34
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
35
- const temp = `${path}.${randomUUID()}.tmp`;
36
- try {
37
- await writeFile(temp, typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
38
- await rename(temp, path);
39
- }
40
- finally {
41
- await rm(temp, { force: true });
42
- }
43
- }
44
- export function updatePaths(base, scope) {
45
- return {
46
- record: join(base, 'openmerit-install.json'), runner: join(base, 'openmerit-updater.mts'),
47
- settings: join(base, scope === 'user' ? 'settings.json' : 'settings.local.json'),
48
- state: join(base, 'openmerit-updates.json'), backup: join(base, 'openmerit-update-backup.json'),
49
- skill: join(base, 'skills/openmerit-scout/SKILL.md'), rule: join(base, 'rules/openmerit-scout.md'),
50
- };
51
- }
52
- function updateRecord(record) {
53
- const value = record.updates;
54
- if (!isObject(value) || typeof value.enabled !== 'boolean' || typeof value.version !== 'string'
55
- || typeof value.runnerHash !== 'string' || !isObject(value.hook))
56
- return null;
57
- return value;
58
- }
59
- // Serialize installs, removals and hooks. A dead process's lock is recoverable;
60
- // an active login is not mistaken for a stale updater just because it is slow.
61
- export async function withUpdateLock(base, work) {
62
- await mkdir(base, { recursive: true, mode: 0o700 });
63
- const path = join(base, 'openmerit-update.lock');
64
- let handle;
65
- for (let attempt = 0; attempt < 2; attempt++) {
66
- try {
67
- handle = await open(path, 'wx', 0o600);
68
- break;
69
- }
70
- catch (error) {
71
- if (!isObject(error) || error.code !== 'EEXIST')
72
- throw error;
73
- // Only one process may reap a dead lock. Without this guard, two stale
74
- // observers can unlink each other's newly acquired live lock.
75
- const recoveryPath = `${path}.recovery`;
76
- let recovery;
77
- try {
78
- recovery = await open(recoveryPath, 'wx', 0o600);
79
- }
80
- catch {
81
- throw new Error('Another OpenMerit lock recovery is running. If it was interrupted, review openmerit-update.lock.recovery before removing it.');
82
- }
83
- try {
84
- await recovery.writeFile(JSON.stringify({ pid: process.pid }));
85
- const text = await optional(path);
86
- if (text === null)
87
- continue;
88
- let alive = true;
89
- try {
90
- const pid = parsed(text).pid;
91
- if (typeof pid === 'number' && pid > 0) {
92
- try {
93
- process.kill(pid, 0);
94
- }
95
- catch (failure) {
96
- if (isObject(failure) && failure.code === 'ESRCH')
97
- alive = false;
98
- }
99
- }
100
- else
101
- alive = Date.now() - (await stat(path)).mtimeMs < 10 * 60_000;
102
- }
103
- catch {
104
- alive = Date.now() - (await stat(path)).mtimeMs < 10 * 60_000;
105
- }
106
- if (alive)
107
- throw new Error('Another OpenMerit install or update is running. Retry shortly.');
108
- await rm(path, { force: true });
109
- }
110
- finally {
111
- await recovery.close();
112
- await rm(recoveryPath, { force: true });
113
- }
114
- }
115
- }
116
- if (!handle)
117
- throw new Error('Another OpenMerit update is running.');
118
- try {
119
- await handle.writeFile(JSON.stringify({ pid: process.pid }));
120
- return await work();
121
- }
122
- finally {
123
- await handle.close();
124
- await rm(path, { force: true });
125
- }
126
- }
127
- function groups(settings) {
128
- if (settings.hooks === undefined)
129
- return {};
130
- if (!isObject(settings.hooks))
131
- throw new Error('Claude settings.hooks is not an object; nothing changed.');
132
- return settings.hooks;
133
- }
134
- function eventGroups(hooks, event) {
135
- const value = hooks[event];
136
- if (value === undefined)
137
- return [];
138
- if (!Array.isArray(value))
139
- throw new Error(`Claude ${event} hooks are not an array; nothing changed.`);
140
- return value;
141
- }
142
- function usesRunner(group, runner) {
143
- return isObject(group) && Array.isArray(group.hooks) && group.hooks.some(hook => isObject(hook) && typeof hook.command === 'string' && (hook.command.includes(runner) || hook.command.includes(quote(runner))));
144
- }
145
- export async function inspectUpdates(base, scope, record) {
146
- const paths = updatePaths(base, scope), saved = updateRecord(record);
147
- const pending = updateRecord({ updates: record.pendingUpdates });
148
- const owners = [saved, pending].filter(value => value !== null);
149
- const runner = await optional(paths.runner), settings = await object(paths.settings), hooks = groups(settings);
150
- const conflicts = [];
151
- if (runner !== null && !owners.some(owner => digest(runner) === owner.runnerHash))
152
- conflicts.push('OpenMerit updater was edited or is not owned; nothing changed.');
153
- for (const event of ['SessionStart', 'Stop']) {
154
- for (const group of eventGroups(hooks, event)) {
155
- if (usesRunner(group, paths.runner) && !owners.some(owner => JSON.stringify(group) === JSON.stringify(owner.hook))) {
156
- conflicts.push(`OpenMerit ${event} hook was edited or is not owned; nothing changed.`);
157
- }
158
- }
159
- }
160
- const active = runner !== null && ['SessionStart', 'Stop'].every(event => eventGroups(hooks, event).some(group => owners.some(owner => JSON.stringify(group) === JSON.stringify(owner.hook))));
161
- return { paths, saved: saved ?? pending, runner, settings, hooks, conflicts, active };
162
- }
163
- // Claude Code uses a shell for command hooks, including Git Bash on Windows.
164
- const quote = (value) => `'${value.replaceAll("'", "'\"'\"'")}'`;
165
- export async function configureUpdates(base, scope, record, version, enabled) {
166
- const state = await inspectUpdates(base, scope, record);
167
- if (state.conflicts.length)
168
- throw new Error(state.conflicts.join('\n'));
169
- // This module is the runner: updates.ts in source, the compiled updates.js in the npm package.
170
- const source = await readFile(new URL(import.meta.url), 'utf8');
171
- const hook = { hooks: [{ type: 'command', command: `${quote(process.execPath)} ${quote(state.paths.runner)} --base ${quote(base)} --scope ${scope} --hook`, async: true, timeout: 150 }] };
172
- const hooks = { ...state.hooks };
173
- for (const event of ['SessionStart', 'Stop']) {
174
- const kept = eventGroups(hooks, event).filter(group => !usesRunner(group, state.paths.runner));
175
- if (enabled)
176
- kept.push(hook);
177
- if (kept.length)
178
- hooks[event] = kept;
179
- else
180
- delete hooks[event];
181
- }
182
- // Avoid clobbering settings that changed while the installer was signing in.
183
- const latest = await object(state.paths.settings);
184
- if (JSON.stringify(latest) !== JSON.stringify(state.settings))
185
- throw new Error('Claude settings changed during setup. Retry.');
186
- const next = { enabled, version, runnerHash: digest(source), hook };
187
- await atomic(state.paths.record, { ...await object(state.paths.record), pendingUpdates: next });
188
- await atomic(state.paths.runner, source);
189
- await atomic(state.paths.settings, { ...state.settings, hooks });
190
- return next;
191
- }
192
- export async function removeUpdates(base, scope, record) {
193
- const state = await inspectUpdates(base, scope, record);
194
- const hooks = { ...state.hooks };
195
- for (const event of ['SessionStart', 'Stop']) {
196
- const kept = eventGroups(hooks, event).filter(group => !state.saved || JSON.stringify(group) !== JSON.stringify(state.saved.hook));
197
- if (kept.length)
198
- hooks[event] = kept;
199
- else
200
- delete hooks[event];
201
- }
202
- if (state.saved)
203
- await atomic(state.paths.settings, { ...state.settings, hooks });
204
- const stillReferenced = ['SessionStart', 'Stop'].some(event => eventGroups(hooks, event).some(group => usesRunner(group, state.paths.runner)));
205
- if (!stillReferenced && state.runner !== null && state.saved && digest(state.runner) === state.saved.runnerHash)
206
- await rm(state.paths.runner, { force: true });
207
- await rm(state.paths.state, { force: true });
208
- }
209
- // Read one fixed regular file in memory, never extract archive paths onto disk.
210
- function instructionBundle(bytes) {
211
- const tar = gunzipSync(bytes, { maxOutputLength: 32 * 1024 * 1024 });
212
- let found = null;
213
- for (let offset = 0; offset + 512 <= tar.length;) {
214
- const header = tar.subarray(offset, offset + 512);
215
- if (header.every(byte => byte === 0))
216
- break;
217
- const name = header.subarray(0, 100).toString().replace(/\0.*$/s, '');
218
- const rawSize = header.subarray(124, 136).toString().replace(/\0.*$/s, '').trim();
219
- if (!/^[0-7]+$/.test(rawSize))
220
- throw new Error('Invalid package archive.');
221
- const size = Number.parseInt(rawSize, 8), start = offset + 512;
222
- if (start + size > tar.length)
223
- throw new Error('Truncated package archive.');
224
- if (name === 'package/dist/terminal/scout-update.json') {
225
- if (found || size > 256 * 1024 || ![0, 48].includes(header[156] ?? -1))
226
- throw new Error('Invalid instruction bundle.');
227
- found = parsed(tar.subarray(start, start + size).toString('utf8'));
228
- }
229
- offset = start + Math.ceil(size / 512) * 512;
230
- }
231
- return found;
232
- }
233
- const stableVersion = (version) => /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version);
234
- function newer(next, installed) {
235
- if (!stableVersion(next) || !stableVersion(installed))
236
- return false;
237
- const a = next.split('.').map(Number), b = installed.split('.').map(Number);
238
- for (let i = 0; i < 3; i++) {
239
- if (a[i] !== b[i])
240
- return a[i] > b[i];
241
- }
242
- return false;
243
- }
244
- async function boundedFetch(url, max, fetcher, timeout = 12_000) {
245
- const controller = new AbortController();
246
- let reader;
247
- let timer;
248
- const deadline = new Promise((_, reject) => {
249
- timer = setTimeout(() => { controller.abort(); reject(new Error('Update download timed out.')); }, timeout);
250
- });
251
- const download = async () => {
252
- const response = await fetcher(url, { redirect: 'error', signal: controller.signal, headers: { Accept: 'application/json' } });
253
- if (!response.ok || !response.body || controller.signal.aborted)
254
- throw new Error('Update server unavailable.');
255
- const parts = [];
256
- let size = 0;
257
- reader = response.body.getReader();
258
- for (;;) {
259
- const { value, done } = await reader.read();
260
- if (done)
261
- break;
262
- size += value.length;
263
- if (size > max)
264
- throw new Error('Update download exceeded its size limit.');
265
- parts.push(value);
266
- }
267
- return Buffer.concat(parts);
268
- };
269
- try {
270
- return await Promise.race([download(), deadline]);
271
- }
272
- finally {
273
- clearTimeout(timer);
274
- controller.abort();
275
- // Cancellation itself must not keep a timed-out hook alive.
276
- void reader?.cancel().catch(() => { });
277
- }
278
- }
279
- async function recover(base, scope) {
280
- const paths = updatePaths(base, scope), text = await optional(paths.backup);
281
- if (!text)
282
- return;
283
- const backup = parsed(text);
284
- if (typeof backup.skill !== 'string' || typeof backup.rule !== 'string' || !isObject(backup.record))
285
- throw new Error('Update recovery needs manual review.');
286
- for (const field of ['skill', 'rule']) {
287
- const current = await optional(paths[field]);
288
- if (current !== null && digest(current) !== digest(backup[field]) && digest(current) !== backup[`${field}Hash`])
289
- throw new Error('An interrupted update has user edits; nothing changed.');
290
- }
291
- // Recover the previous complete pair before any retry. Atomic per-file writes
292
- // plus a durable journal cover failure between the two renames.
293
- await atomic(paths.skill, backup.skill);
294
- await atomic(paths.rule, backup.rule);
295
- await atomic(paths.record, backup.record);
296
- await rm(paths.backup, { force: true });
297
- }
298
- export async function recoverUpdates(base, scope) { await recover(base, scope); }
299
- export async function runUpdate(base, scope, options = {}) {
300
- return withUpdateLock(base, async () => {
301
- await recover(base, scope);
302
- const paths = updatePaths(base, scope), record = await object(paths.record), saved = updateRecord(record);
303
- if (record.version !== 3 || record.scope !== scope || !saved)
304
- return { status: 'manual_required', message: 'Run npx -y openmerit@latest install --client claude-code to bootstrap updates in this scope.' };
305
- if (options.hook && (!saved.enabled || process.env.OPENMERIT_AUTO_UPDATE === 'off'))
306
- return { status: 'disabled', message: 'Automatic updates disabled.' };
307
- const now = options.now ?? Date.now(), state = options.enabled === undefined ? await object(paths.state) : {};
308
- if (options.hook && typeof state.nextCheckAt === 'number' && now < state.nextCheckAt)
309
- return { status: 'not_due', message: 'Update check not due.' };
310
- const support = await inspectUpdates(base, scope, record);
311
- if (support.conflicts.length) {
312
- if (options.hook)
313
- await atomic(paths.state, { nextCheckAt: now + day, status: 'conflict' });
314
- return { status: 'conflict', message: `${support.conflicts.join(' ')} Say “update openmerit” after resolving the conflict.` };
315
- }
316
- if (options.enabled !== undefined) {
317
- record.updates = await configureUpdates(base, scope, record, saved.version, options.enabled);
318
- delete record.pendingUpdates;
319
- await atomic(paths.record, record);
320
- return { status: options.enabled ? 'enabled' : 'disabled', message: `Automatic Scout instruction updates ${options.enabled ? 'enabled' : 'disabled'}. Start a new Claude Code session to reload hooks.` };
321
- }
322
- // Record the attempt before networking so a crashed hook cannot hammer npm.
323
- await atomic(paths.state, { ...state, nextCheckAt: now + retry });
324
- try {
325
- const fetcher = options.fetcher ?? fetch;
326
- const release = parsed((await boundedFetch(registry, 512 * 1024, fetcher)).toString('utf8'));
327
- const version = release.version;
328
- if (release.name !== 'openmerit' || typeof version !== 'string' || !stableVersion(version))
329
- throw new Error('Invalid stable release metadata.');
330
- if (!newer(version, saved.version)) {
331
- await atomic(paths.state, { checkedAt: now, nextCheckAt: now + day, status: 'current' });
332
- return { status: 'current', version: saved.version, message: `Scout instructions ${saved.version} are current.` };
333
- }
334
- const dist = release.dist;
335
- const tarball = `https://registry.npmjs.org/openmerit/-/openmerit-${version}.tgz`;
336
- if (!isObject(dist) || dist.tarball !== tarball || typeof dist.integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(dist.integrity))
337
- throw new Error('Release has no trusted SHA-512 package integrity.');
338
- const bytes = await boundedFetch(tarball, 16 * 1024 * 1024, fetcher, 120_000);
339
- if (`sha512-${createHash('sha512').update(bytes).digest('base64')}` !== dist.integrity)
340
- throw new Error('Package integrity verification failed.');
341
- const bundle = instructionBundle(bytes);
342
- if (!bundle || bundle.format !== 1 || bundle.contract !== updateContract || bundle.version !== version) {
343
- await atomic(paths.state, { checkedAt: now, nextCheckAt: now + day, status: 'manual_required' });
344
- return { status: 'manual_required', version, message: `Release ${version} needs a reviewed installer upgrade. Review its changes, then run npx -y openmerit@latest install --client claude-code --scope ${scope}. Do not bypass permissions or rerun login automatically.` };
345
- }
346
- if (typeof bundle.skill !== 'string' || typeof bundle.rule !== 'string' || !bundle.skill.startsWith('---\nname: openmerit-scout\n') || !bundle.rule.trim())
347
- throw new Error('Invalid Scout instructions.');
348
- const instructions = bundle;
349
- const skill = await optional(paths.skill), rule = await optional(paths.rule);
350
- if (skill === null || rule === null || digest(skill) !== record.skillHash || digest(rule) !== record.ruleHash) {
351
- await atomic(paths.state, { checkedAt: now, nextCheckAt: now + day, status: 'conflict' });
352
- return { status: 'conflict', message: 'Scout instructions are missing or edited. Nothing changed. Say “update openmerit” after reviewing the files.' };
353
- }
354
- const skillHash = digest(instructions.skill), ruleHash = digest(instructions.rule);
355
- await atomic(paths.backup, { record, skill, rule, skillHash, ruleHash });
356
- try {
357
- // Do not overwrite edits made during the download or journal write.
358
- if (await optional(paths.skill) !== skill || await optional(paths.rule) !== rule)
359
- throw new Error('Scout instructions changed during the update.');
360
- await atomic(paths.skill, instructions.skill);
361
- await atomic(paths.rule, instructions.rule);
362
- await atomic(paths.record, { ...record, skillHash, ruleHash, updates: { ...saved, version } });
363
- await rm(paths.backup, { force: true });
364
- }
365
- catch (error) {
366
- try {
367
- await recover(base, scope);
368
- }
369
- catch {
370
- return { status: 'conflict', message: 'An interrupted Scout update could not be recovered safely. Review the installed files and openmerit-update-backup.json; no user edits were overwritten.' };
371
- }
372
- throw error;
373
- }
374
- // A failed schedule write must not misreport an already applied update.
375
- await atomic(paths.state, { checkedAt: now, nextCheckAt: now + day, status: 'updated', version }).catch(() => { });
376
- return { status: 'updated', version, message: `Scout instructions updated to ${version}. Start a new Claude Code session to load them; the running session may still use its previous instructions.` };
377
- }
378
- catch {
379
- await atomic(paths.state, { nextCheckAt: now + retry, status: 'retry' });
380
- return { status: 'retry', message: 'Could not safely update Scout. The installed version was retained. Automatic checks retry after 15 minutes while Claude Code is in use; say “update openmerit” to retry now.' };
381
- }
382
- });
383
- }
384
- // Hooks deliberately never ask Claude to continue, read transcripts or emit
385
- // blocking decisions. Offline and concurrent-hook failures exit successfully.
386
- if (process.argv[1] && realpathSync(resolve(process.argv[1])) === fileURLToPath(import.meta.url)) {
387
- const args = process.argv.slice(2), base = args[args.indexOf('--base') + 1], scope = args[args.indexOf('--scope') + 1];
388
- if (base && ['user', 'project'].includes(scope ?? '')) {
389
- process.stdin.resume();
390
- try {
391
- const result = await runUpdate(resolve(base), scope, { hook: args.includes('--hook') });
392
- if (['updated', 'conflict', 'manual_required', 'retry'].includes(result.status))
393
- console.log(JSON.stringify({ systemMessage: result.message }));
394
- }
395
- catch { /* Maintenance cannot block the user's task. */ }
396
- }
397
- }
@@ -1,15 +0,0 @@
1
- # Agent-evaluation CLI retirement
2
-
3
- **npm 0.3.0, 4 November 2026.** The standalone `openmerit eval` CLI is retired in full, including `init`, `doctor`, `plan`, `run`, `watch` and `report`. Older installed packages keep it until they are upgraded. The old Scout JSON recommendation API went offline with the Scout serving deploy of 5 November 2026.
4
-
5
- After an approved Scout release, `/scout/api/recommend` and its legacy alpha/versioned paths return 404, regardless of authentication. There is no replacement frozen-plan protocol. Use the [Scout conversational agent or MCP](scout.md) for provisional model guidance and your own representative task checks for evaluation. The Inference Terminal is [retired as well](inference-terminal.md); the Pi adapter remains a native Scout MCP integration, not an evaluation runner.
6
-
7
- ## Existing local artifacts
8
-
9
- Upgrading does not delete `.openmerit/eval.json`, reports, budget ledgers, task fixtures or grader files. Review and retain or remove them yourself. Stop any old foreground watcher before upgrading. Instructions previously appended to `AGENTS.md` or `CLAUDE.md` are not automatically rewritten; remove obsolete evaluation instructions deliberately, preserving unrelated project guidance.
10
-
11
- Historical reports remain evidence only for the versions and tasks actually tested. Unknown costs do not become known, and past budget reservations are not reconciled provider bills. Retirement does not cancel external agent/provider work, erase historical Scout analytics, change provider retention or enforce billing limits.
12
-
13
- ## Preserved capabilities
14
-
15
- Scout's agent, MCP, coding-agent setup, catalogue refresh/publication, status and account/privacy controls remain. Shared evidence preparation and model projections remain available to those consumers. See [commands](commands.md), [security](security.md) and [testing](testing.md) for the remaining interfaces and verification boundary.
package/docs/commands.md DELETED
@@ -1,9 +0,0 @@
1
- # Commands
2
-
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 Pi 0.87 `/openmerit` slash commands were removed with that adapter and have no replacement.
6
-
7
- **Retired in npm 0.3.0:** `openmerit dash` ([details](inference-terminal.md)) and all `openmerit eval` commands, including `plan`. Existing local files are untouched. The Scout serving deploy of 5 November 2026 retired `/scout/api/recommend`; use Scout's conversational agent or MCP. Hosted Workers change only through separate approved deployments. See [retirement details](agent-evaluations.md).
8
-
9
- The [Scout setup commands](scout.md#commands) remain available, beginning with `npx -y openmerit@latest install --client claude-code`.
@@ -1,37 +0,0 @@
1
- # Getting started
2
-
3
- OpenMerit provides Scout model-selection guidance. The Inference Terminal (`openmerit dash`) is [retired in npm 0.3.0](inference-terminal.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
-
5
- ## Connect Scout to a coding agent
6
-
7
- 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:
8
-
9
- ```sh
10
- npx -y openmerit@latest install --client pi --scope user
11
- ```
12
-
13
- 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:
14
-
15
- ```sh
16
- npx -y openmerit@latest install --client pi --scope project
17
- ```
18
-
19
- 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.
20
-
21
- 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.
22
-
23
- ## Check, update, or remove Pi setup
24
-
25
- ```sh
26
- npx -y openmerit@latest status --client pi --scope user --check
27
- npx -y openmerit@latest doctor --client pi --scope user
28
- npx -y openmerit@latest remove --client pi --scope user
29
- ```
30
-
31
- 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.
32
-
33
- 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.
34
-
35
- ## Evaluate application changes
36
-
37
- The standalone evaluation CLI is retired in npm 0.3.0, and the legacy JSON recommendation API went offline with the Scout serving deploy of 5 November 2026. Scout guidance is not measured application quality; use your own task checks and provider budget controls. See [retirement details](agent-evaluations.md).
package/docs/index.md DELETED
@@ -1,25 +0,0 @@
1
- # OpenMerit documentation
2
-
3
- These maintained Markdown guides describe Scout setup and model-selection workflows. The policy, protected memo, Scout service and npm package are released independently. A local documentation build does not publish a hosted change.
4
-
5
- ## Discover model candidates
6
-
7
- Start with [installation and setup](getting-started.md). The [Scout beta](scout.md) provides conversational application-model guidance at [openmerit.ai/scout/](https://openmerit.ai/scout/) and through MCP for coding agents. The legacy prompt-only JSON recommendation endpoint is retired.
8
-
9
- ## Connect Scout to Pi
10
-
11
- 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.
12
-
13
- 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.
14
-
15
- ## Evaluate application changes
16
-
17
- Use your own representative task checks to evaluate model candidates. The [standalone evaluation CLI is retired in npm 0.3.0](agent-evaluations.md); existing local artifacts are not deleted.
18
-
19
- Follow [close the model-selection loop](scout-close-loop.md) to consult, measure,
20
- import aggregates and choose. Protected Langfuse setup requires npm 0.3.5 or newer
21
- and a matching hosted connector; npm 0.3.6 adds saved-key mapping updates.
22
-
23
- ## Operate and troubleshoot
24
-
25
- [Commands](commands.md) lists the Scout and setup commands. The [Inference Terminal is retired in npm 0.3.0](inference-terminal.md); existing local files are not deleted. [Troubleshooting](troubleshooting.md), [security and data](security.md), and [current limitations](roadmap.md) explain setup failures and product boundaries. [Testing and validation](testing.md) distinguishes automated coverage from product evidence.