@haystackeditor/cli 0.15.21 → 0.15.23
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 +39 -2
- package/dist/commands/verify-core.d.ts +26 -1
- package/dist/commands/verify-core.js +59 -23
- package/dist/commands/verify-hosted-mcp.d.ts +12 -0
- package/dist/commands/verify-hosted-mcp.js +234 -0
- package/dist/commands/verify-hosted.d.ts +60 -0
- package/dist/commands/verify-hosted.js +342 -0
- package/dist/commands/verify-mcp.js +59 -0
- package/dist/commands/verify-ops.js +1 -1
- package/dist/commands/verify-reproducibility.d.ts +85 -0
- package/dist/commands/verify-reproducibility.js +494 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +55 -22
- package/dist/index.js +91 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/dist/utils/haystack-api.d.ts +1 -1
- package/dist/utils/haystack-api.js +2 -2
- package/package.json +1 -1
- package/schemas/cloud-verifier.v1.json +96 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { withSchema } from '../schema.js';
|
|
4
|
+
import { resolveAuthContext } from '../utils/auth.js';
|
|
5
|
+
import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
|
|
6
|
+
const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
7
|
+
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
8
|
+
const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
9
|
+
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
10
|
+
const STATUS = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
11
|
+
const TERMINAL_STATUSES = new Set(['complete', 'errored', 'expired', 'terminated']);
|
|
12
|
+
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
13
|
+
const DEFAULT_TIMEOUT_MINUTES = 35;
|
|
14
|
+
function requireRecord(value, context) {
|
|
15
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
|
+
throw new Error(`${context} returned an invalid response object.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function requireString(value, field, pattern) {
|
|
21
|
+
const candidate = value[field];
|
|
22
|
+
if (typeof candidate !== 'string'
|
|
23
|
+
|| candidate.length === 0
|
|
24
|
+
|| (pattern && !pattern.test(candidate))) {
|
|
25
|
+
throw new Error(`Hosted verifier response is missing a valid ${field}.`);
|
|
26
|
+
}
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
function optionalString(value, field) {
|
|
30
|
+
const candidate = value[field];
|
|
31
|
+
if (candidate === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
if (typeof candidate !== 'string' || candidate.length === 0) {
|
|
34
|
+
throw new Error(`Hosted verifier response has an invalid ${field}.`);
|
|
35
|
+
}
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
function optionalTimestamp(value, field) {
|
|
39
|
+
const candidate = optionalString(value, field);
|
|
40
|
+
if (candidate === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const parsed = new Date(candidate);
|
|
43
|
+
if (!Number.isFinite(parsed.getTime())
|
|
44
|
+
|| parsed.toISOString() !== candidate) {
|
|
45
|
+
throw new Error(`Hosted verifier response has an invalid ${field}.`);
|
|
46
|
+
}
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
export function parseHostedRunState(value, expectedRunId) {
|
|
50
|
+
const response = requireRecord(value, 'Hosted verifier');
|
|
51
|
+
const runId = requireString(response, 'runId', RUN_ID);
|
|
52
|
+
if (expectedRunId && runId !== expectedRunId) {
|
|
53
|
+
throw new Error(`Hosted verifier returned run ${runId} while status for ${expectedRunId} was requested.`);
|
|
54
|
+
}
|
|
55
|
+
const status = requireString(response, 'status', STATUS);
|
|
56
|
+
const repository = optionalString(response, 'repository');
|
|
57
|
+
if (repository) {
|
|
58
|
+
const parts = repository.split('/');
|
|
59
|
+
if (parts.length !== 2
|
|
60
|
+
|| !parts.every(part => REPOSITORY_COMPONENT.test(part))) {
|
|
61
|
+
throw new Error('Hosted verifier response has an invalid repository.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const baseCommit = optionalString(response, 'baseCommit');
|
|
65
|
+
const headCommit = optionalString(response, 'headCommit');
|
|
66
|
+
if (baseCommit && !COMMIT_SHA.test(baseCommit)) {
|
|
67
|
+
throw new Error('Hosted verifier response has an invalid baseCommit.');
|
|
68
|
+
}
|
|
69
|
+
if (headCommit && !COMMIT_SHA.test(headCommit)) {
|
|
70
|
+
throw new Error('Hosted verifier response has an invalid headCommit.');
|
|
71
|
+
}
|
|
72
|
+
const outputValue = response.output;
|
|
73
|
+
const output = outputValue === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: requireRecord(outputValue, 'Hosted verifier output');
|
|
76
|
+
const createdAt = optionalTimestamp(response, 'createdAt');
|
|
77
|
+
const completedAt = optionalTimestamp(response, 'completedAt');
|
|
78
|
+
if (status === 'complete') {
|
|
79
|
+
if (!repository
|
|
80
|
+
|| !baseCommit
|
|
81
|
+
|| !headCommit
|
|
82
|
+
|| !createdAt
|
|
83
|
+
|| !completedAt
|
|
84
|
+
|| !output
|
|
85
|
+
|| output.runId !== runId
|
|
86
|
+
|| output.repository !== repository
|
|
87
|
+
|| output.baseCommit !== baseCommit
|
|
88
|
+
|| output.headCommit !== headCommit
|
|
89
|
+
|| output.sourceOfflineReady !== true
|
|
90
|
+
|| output.sandboxDeleted !== true) {
|
|
91
|
+
throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
runId,
|
|
96
|
+
status,
|
|
97
|
+
repository,
|
|
98
|
+
baseCommit,
|
|
99
|
+
headCommit,
|
|
100
|
+
createdAt,
|
|
101
|
+
completedAt,
|
|
102
|
+
output,
|
|
103
|
+
error: optionalString(response, 'error'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function parseHostedRepository(value) {
|
|
107
|
+
const parts = value.split('/');
|
|
108
|
+
if (parts.length !== 2
|
|
109
|
+
|| !parts.every(part => REPOSITORY_COMPONENT.test(part))) {
|
|
110
|
+
throw new Error(`Invalid repository "${value}". Use an exact GitHub owner/repository name.`);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
owner: parts[0],
|
|
114
|
+
repository: parts[1],
|
|
115
|
+
fullName: value,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function requireCommit(value, flag) {
|
|
119
|
+
if (typeof value !== 'string' || !COMMIT_SHA.test(value)) {
|
|
120
|
+
throw new Error(`${flag} must be an exact 40-character lowercase commit SHA.`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit) {
|
|
125
|
+
const digest = createHash('sha256')
|
|
126
|
+
.update(JSON.stringify([repository, baseCommit, headCommit]))
|
|
127
|
+
.digest('hex');
|
|
128
|
+
return `cli:${digest}`;
|
|
129
|
+
}
|
|
130
|
+
function requireIdempotencyKey(value) {
|
|
131
|
+
if (!IDEMPOTENCY_KEY.test(value)) {
|
|
132
|
+
throw new Error('--idempotency-key must be 8-128 characters and use only letters, numbers, ".", "_", ":", or "-".');
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function parsePositiveNumber(value, fallback, flag, maximum) {
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
return fallback;
|
|
139
|
+
const parsed = Number(value);
|
|
140
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > maximum) {
|
|
141
|
+
throw new Error(`${flag} must be a positive number no greater than ${maximum}.`);
|
|
142
|
+
}
|
|
143
|
+
return parsed;
|
|
144
|
+
}
|
|
145
|
+
function pollingOptions(interval, timeout, onStatus) {
|
|
146
|
+
return {
|
|
147
|
+
intervalMs: parsePositiveNumber(interval, DEFAULT_INTERVAL_SECONDS, '--interval', 300) * 1000,
|
|
148
|
+
timeoutMs: parsePositiveNumber(timeout, DEFAULT_TIMEOUT_MINUTES, '--timeout', 1_440) * 60_000,
|
|
149
|
+
onStatus,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
export async function startHostedVerification(repositoryArg, options, token) {
|
|
153
|
+
const repository = parseHostedRepository(repositoryArg);
|
|
154
|
+
const baseCommit = requireCommit(options.base, '--base');
|
|
155
|
+
const headCommit = requireCommit(options.head, '--head');
|
|
156
|
+
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
157
|
+
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit));
|
|
158
|
+
const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
owner: repository.owner,
|
|
163
|
+
repository: repository.repository,
|
|
164
|
+
baseCommit,
|
|
165
|
+
headCommit,
|
|
166
|
+
idempotencyKey,
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
const state = parseHostedRunState(response);
|
|
170
|
+
return {
|
|
171
|
+
state: {
|
|
172
|
+
...state,
|
|
173
|
+
repository: state.repository ?? repository.fullName,
|
|
174
|
+
baseCommit: state.baseCommit ?? baseCommit,
|
|
175
|
+
headCommit: state.headCommit ?? headCommit,
|
|
176
|
+
},
|
|
177
|
+
idempotencyKey,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export async function getHostedVerification(runId, token) {
|
|
181
|
+
if (!RUN_ID.test(runId)) {
|
|
182
|
+
throw new Error('Run id must match cv_ followed by 48 lowercase hex characters.');
|
|
183
|
+
}
|
|
184
|
+
let response;
|
|
185
|
+
try {
|
|
186
|
+
response = await haystackJson(`/api/agent/cloud-verifier/runs/${encodeURIComponent(runId)}`, token, {}, [410]);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
if (error instanceof HaystackApiError && error.status === 404) {
|
|
190
|
+
throw new Error(`Hosted verifier run ${runId} was not found for the selected account. `
|
|
191
|
+
+ 'If the run was started with another saved account, retry with `--account <login>`.');
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
return parseHostedRunState(response, runId);
|
|
196
|
+
}
|
|
197
|
+
function delay(milliseconds) {
|
|
198
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
199
|
+
}
|
|
200
|
+
export async function waitForHostedVerification(runId, token, options) {
|
|
201
|
+
const startedAt = Date.now();
|
|
202
|
+
let lastStatus;
|
|
203
|
+
while (true) {
|
|
204
|
+
const state = await getHostedVerification(runId, token);
|
|
205
|
+
if (state.status !== lastStatus) {
|
|
206
|
+
options.onStatus?.(state);
|
|
207
|
+
lastStatus = state.status;
|
|
208
|
+
}
|
|
209
|
+
if (TERMINAL_STATUSES.has(state.status)) {
|
|
210
|
+
return { state, timedOut: false };
|
|
211
|
+
}
|
|
212
|
+
const elapsed = Date.now() - startedAt;
|
|
213
|
+
if (elapsed >= options.timeoutMs) {
|
|
214
|
+
return { state, timedOut: true };
|
|
215
|
+
}
|
|
216
|
+
await delay(Math.min(options.intervalMs, options.timeoutMs - elapsed));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
export function buildHostedJson(operation, state, extras = {}) {
|
|
220
|
+
return withSchema('cloud-verifier', {
|
|
221
|
+
operation,
|
|
222
|
+
run_id: state.runId,
|
|
223
|
+
status: state.status,
|
|
224
|
+
...(state.repository ? { repository: state.repository } : {}),
|
|
225
|
+
...(state.baseCommit ? { base_commit: state.baseCommit } : {}),
|
|
226
|
+
...(state.headCommit ? { head_commit: state.headCommit } : {}),
|
|
227
|
+
...(state.createdAt ? { created_at: state.createdAt } : {}),
|
|
228
|
+
...(state.completedAt ? { completed_at: state.completedAt } : {}),
|
|
229
|
+
...(extras.account ? { account: extras.account } : {}),
|
|
230
|
+
...(extras.idempotencyKey
|
|
231
|
+
? { idempotency_key: extras.idempotencyKey }
|
|
232
|
+
: {}),
|
|
233
|
+
...(extras.nextCommand ? { next_command: extras.nextCommand } : {}),
|
|
234
|
+
...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
|
|
235
|
+
...(state.output ? { output: state.output } : {}),
|
|
236
|
+
...(state.error ? { error: state.error } : {}),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
export function hostedResumeCommand(runId, account) {
|
|
240
|
+
return `haystack verify hosted status ${runId}${account ? ` --account ${account}` : ''} --wait`;
|
|
241
|
+
}
|
|
242
|
+
function printHostedState(operation, state, options) {
|
|
243
|
+
const nextCommand = TERMINAL_STATUSES.has(state.status)
|
|
244
|
+
? undefined
|
|
245
|
+
: hostedResumeCommand(state.runId, options.account);
|
|
246
|
+
if (options.json) {
|
|
247
|
+
process.stdout.write(`${JSON.stringify(buildHostedJson(operation, state, {
|
|
248
|
+
account: options.account,
|
|
249
|
+
idempotencyKey: options.idempotencyKey,
|
|
250
|
+
nextCommand,
|
|
251
|
+
timedOut: options.timedOut,
|
|
252
|
+
}), null, 2)}\n`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const icon = state.status === 'complete'
|
|
256
|
+
? chalk.green('✓')
|
|
257
|
+
: TERMINAL_STATUSES.has(state.status)
|
|
258
|
+
? chalk.red('✗')
|
|
259
|
+
: chalk.cyan('•');
|
|
260
|
+
console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
|
|
261
|
+
if (state.repository) {
|
|
262
|
+
console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
|
|
263
|
+
}
|
|
264
|
+
if (state.baseCommit && state.headCommit) {
|
|
265
|
+
console.log(` ${chalk.dim('Commits:')} ${state.baseCommit.slice(0, 12)} → ${state.headCommit.slice(0, 12)}`);
|
|
266
|
+
}
|
|
267
|
+
if (state.error)
|
|
268
|
+
console.log(` ${chalk.dim('Error:')} ${state.error}`);
|
|
269
|
+
if (options.timedOut) {
|
|
270
|
+
console.log(chalk.yellow(' Wait timed out; the production run is still active.'));
|
|
271
|
+
}
|
|
272
|
+
if (nextCommand)
|
|
273
|
+
console.log(` ${chalk.dim('Resume:')} ${nextCommand}`);
|
|
274
|
+
}
|
|
275
|
+
function markNonSuccess(state, timedOut = false) {
|
|
276
|
+
if (timedOut
|
|
277
|
+
|| (state.status !== 'complete' && TERMINAL_STATUSES.has(state.status))) {
|
|
278
|
+
process.exitCode = 2;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
282
|
+
const repository = parseHostedRepository(repositoryArg);
|
|
283
|
+
const auth = await resolveAuthContext({
|
|
284
|
+
preferredLogin: options.account,
|
|
285
|
+
owner: repository.owner,
|
|
286
|
+
repo: repository.repository,
|
|
287
|
+
});
|
|
288
|
+
if (!options.json) {
|
|
289
|
+
console.error(chalk.dim('Submitting the exact commit pair to the hosted verifier…'));
|
|
290
|
+
}
|
|
291
|
+
const started = await startHostedVerification(repositoryArg, options, auth.token);
|
|
292
|
+
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
293
|
+
? undefined
|
|
294
|
+
: auth.login;
|
|
295
|
+
let state = started.state;
|
|
296
|
+
let timedOut;
|
|
297
|
+
if (options.wait !== false && !TERMINAL_STATUSES.has(state.status)) {
|
|
298
|
+
const waited = await waitForHostedVerification(state.runId, auth.token, pollingOptions(options.interval, options.timeout, current => {
|
|
299
|
+
if (!options.json) {
|
|
300
|
+
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
301
|
+
}
|
|
302
|
+
}));
|
|
303
|
+
state = waited.state;
|
|
304
|
+
timedOut = waited.timedOut;
|
|
305
|
+
}
|
|
306
|
+
else if (options.wait !== false) {
|
|
307
|
+
timedOut = false;
|
|
308
|
+
}
|
|
309
|
+
printHostedState('start', state, {
|
|
310
|
+
json: options.json,
|
|
311
|
+
account: selectedAccount,
|
|
312
|
+
idempotencyKey: started.idempotencyKey,
|
|
313
|
+
timedOut,
|
|
314
|
+
});
|
|
315
|
+
markNonSuccess(state, timedOut);
|
|
316
|
+
}
|
|
317
|
+
export async function verifyHostedStatusCommand(runId, options) {
|
|
318
|
+
const auth = await resolveAuthContext({ preferredLogin: options.account });
|
|
319
|
+
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
320
|
+
? undefined
|
|
321
|
+
: auth.login;
|
|
322
|
+
let state = await getHostedVerification(runId, auth.token);
|
|
323
|
+
let timedOut;
|
|
324
|
+
if (options.wait && !TERMINAL_STATUSES.has(state.status)) {
|
|
325
|
+
const waited = await waitForHostedVerification(runId, auth.token, pollingOptions(options.interval, options.timeout, current => {
|
|
326
|
+
if (!options.json) {
|
|
327
|
+
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
328
|
+
}
|
|
329
|
+
}));
|
|
330
|
+
state = waited.state;
|
|
331
|
+
timedOut = waited.timedOut;
|
|
332
|
+
}
|
|
333
|
+
else if (options.wait) {
|
|
334
|
+
timedOut = false;
|
|
335
|
+
}
|
|
336
|
+
printHostedState('status', state, {
|
|
337
|
+
json: options.json,
|
|
338
|
+
account: selectedAccount,
|
|
339
|
+
timedOut,
|
|
340
|
+
});
|
|
341
|
+
markNonSuccess(state, timedOut);
|
|
342
|
+
}
|
|
@@ -99,6 +99,19 @@ export const VERIFY_TOOLS = [
|
|
|
99
99
|
required: ['cell'],
|
|
100
100
|
},
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
name: 'verify_universe',
|
|
104
|
+
description: 'Get or open a live universe for one test: returns the running environment when one is still live, otherwise reopens the retained state onto fresh compute (reusing an existing attachment when present).',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
run: runInput,
|
|
109
|
+
cell: { type: 'string', description: 'Stable test selector: ordinal (1-based), riskCellId, or exact planner cellKey.' },
|
|
110
|
+
side: { type: 'string', enum: ['base', 'head'], description: 'Matched universe side (default head).' },
|
|
111
|
+
},
|
|
112
|
+
required: ['cell'],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
102
115
|
{
|
|
103
116
|
name: 'verify_cleanup',
|
|
104
117
|
description: 'Read retained-state cleanup for one test universe by its exact fork ID: shared references, provider deletion attempts, and the durable cleanup-SLO verdict.',
|
|
@@ -292,6 +305,52 @@ export async function runVerifyMcpServer(options) {
|
|
|
292
305
|
return ok(match.cell);
|
|
293
306
|
return ok(cellDetail(loaded.run, match));
|
|
294
307
|
}
|
|
308
|
+
if (name === 'verify_universe') {
|
|
309
|
+
if (typeof input.cell !== 'string')
|
|
310
|
+
return fail('cell required');
|
|
311
|
+
if (input.side !== undefined
|
|
312
|
+
&& input.side !== 'base'
|
|
313
|
+
&& input.side !== 'head') {
|
|
314
|
+
return fail('side must be base or head');
|
|
315
|
+
}
|
|
316
|
+
const loaded = await loadRunAuto({ server, repoRoot, selector });
|
|
317
|
+
const match = findCellByStableSelector(loaded.run, input.cell);
|
|
318
|
+
const side = input.side === 'base' ? 'base' : 'head';
|
|
319
|
+
const universe = side === 'base' ? match.cell.base : match.cell.head;
|
|
320
|
+
const expired = universe.expiresAt
|
|
321
|
+
? Date.parse(universe.expiresAt) < Date.now()
|
|
322
|
+
: false;
|
|
323
|
+
if (universe.browserUrl && universe.status === 'running' && !expired) {
|
|
324
|
+
return ok({
|
|
325
|
+
schema_version: '1.0.0',
|
|
326
|
+
run_id: loaded.run.runId,
|
|
327
|
+
ordinal: match.cell.ordinal,
|
|
328
|
+
side,
|
|
329
|
+
source: 'running',
|
|
330
|
+
url: universe.browserUrl,
|
|
331
|
+
expires_at: universe.expiresAt ?? null,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (!universe.forkId) {
|
|
335
|
+
return fail(`Test ${match.cell.ordinal}'s ${side} universe is not running and `
|
|
336
|
+
+ 'has no durable fork ID to reopen.');
|
|
337
|
+
}
|
|
338
|
+
const { requestUniverseReopen } = await import('./verify-ops.js');
|
|
339
|
+
const reopened = await requestUniverseReopen(server, universe.forkId);
|
|
340
|
+
return ok({
|
|
341
|
+
schema_version: '1.0.0',
|
|
342
|
+
run_id: loaded.run.runId,
|
|
343
|
+
ordinal: match.cell.ordinal,
|
|
344
|
+
side,
|
|
345
|
+
source: 'reopened',
|
|
346
|
+
url: reopened.browserUrl,
|
|
347
|
+
expires_at: reopened.expiresAt,
|
|
348
|
+
fork_id: reopened.forkId,
|
|
349
|
+
sandbox_id: reopened.sandboxId,
|
|
350
|
+
state_expires_at: reopened.stateExpiresAt,
|
|
351
|
+
reused: reopened.reused,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
295
354
|
if (name === 'verify_cleanup') {
|
|
296
355
|
if (typeof input.cell !== 'string')
|
|
297
356
|
return fail('cell required');
|
|
@@ -26,7 +26,7 @@ function renderStatus(summary) {
|
|
|
26
26
|
: summary.status === 'complete' ? chalk.green(summary.status)
|
|
27
27
|
: summary.status === 'failed' ? chalk.red(summary.status)
|
|
28
28
|
: chalk.cyan(summary.status);
|
|
29
|
-
console.log(`${chalk.bold(summary.run_id)} — ${state}${summary.age ? chalk.dim(` (
|
|
29
|
+
console.log(`${chalk.bold(summary.run_id)} — ${state}${summary.age ? chalk.dim(` (started ${summary.age} ago)`) : ''}`);
|
|
30
30
|
for (const chapter of summary.chapters) {
|
|
31
31
|
console.log(`${chalk.dim(chapter.repository)} ${chapter.pull_request} — ${chapter.label}`);
|
|
32
32
|
if (chapter.execution_mode === 'bisect' && chapter.bisect) {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { type RiskCell, type VerificationRun } from "./verify-core.js";
|
|
2
|
+
import { type VerifyOptions } from "./verify.js";
|
|
3
|
+
export type ReproducibilityScope = "executor" | "planner" | "full";
|
|
4
|
+
export interface CellOutcomeObservation {
|
|
5
|
+
run_id: string;
|
|
6
|
+
status: string;
|
|
7
|
+
verdict: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface CellPlanObservation {
|
|
10
|
+
run_id: string;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ReproducibilityCell {
|
|
14
|
+
cell_id: string;
|
|
15
|
+
chapter_id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
outcomes: CellOutcomeObservation[];
|
|
18
|
+
plans: CellPlanObservation[];
|
|
19
|
+
missing_run_ids: string[];
|
|
20
|
+
}
|
|
21
|
+
export interface BisectOutcomeObservation {
|
|
22
|
+
run_id: string;
|
|
23
|
+
status: string;
|
|
24
|
+
culprit_sha: string | null;
|
|
25
|
+
parent_sha: string | null;
|
|
26
|
+
incident: {
|
|
27
|
+
commit: string;
|
|
28
|
+
status: string;
|
|
29
|
+
} | null;
|
|
30
|
+
probes: Array<{
|
|
31
|
+
commit: string;
|
|
32
|
+
status: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface ReproducibilityBisect {
|
|
36
|
+
chapter_id: string;
|
|
37
|
+
label: string;
|
|
38
|
+
outcomes: BisectOutcomeObservation[];
|
|
39
|
+
missing_run_ids: string[];
|
|
40
|
+
}
|
|
41
|
+
export interface ReproducibilityReport {
|
|
42
|
+
schema_version: "1.0.0";
|
|
43
|
+
scope: ReproducibilityScope;
|
|
44
|
+
requested_runs: number;
|
|
45
|
+
completed_runs: number;
|
|
46
|
+
run_ids: string[];
|
|
47
|
+
plan_source_run_id: string | null;
|
|
48
|
+
compared_cells: number;
|
|
49
|
+
stable_cells: number;
|
|
50
|
+
compared_bisects: number;
|
|
51
|
+
stable_bisects: number;
|
|
52
|
+
plan_drift_cells: ReproducibilityCell[];
|
|
53
|
+
outcome_drift_cells: ReproducibilityCell[];
|
|
54
|
+
unrunnable_cells: ReproducibilityCell[];
|
|
55
|
+
bisect_drift_chapters: ReproducibilityBisect[];
|
|
56
|
+
unrunnable_bisects: ReproducibilityBisect[];
|
|
57
|
+
planner_incomplete_run_ids: string[];
|
|
58
|
+
executor_incomplete_run_ids: string[];
|
|
59
|
+
incomplete_run_ids: string[];
|
|
60
|
+
timed_out_run_ids: string[];
|
|
61
|
+
planner_reproducible: boolean;
|
|
62
|
+
executor_reproducible: boolean;
|
|
63
|
+
reproducible: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function cellPlanFingerprint(cell: RiskCell): string;
|
|
66
|
+
/**
|
|
67
|
+
* Plan identity and runtime outcomes are independent axes:
|
|
68
|
+
*
|
|
69
|
+
* - missing IDs or changed frozen contracts are planner drift;
|
|
70
|
+
* - status/judgment changes under an identical contract are executor drift.
|
|
71
|
+
*
|
|
72
|
+
* Titles and other prose are fingerprinted to detect a changed judge input,
|
|
73
|
+
* but cells are correlated only by their exact stable IDs.
|
|
74
|
+
*/
|
|
75
|
+
export declare function analyzeReproducibility(runs: VerificationRun[], requestedRuns?: number, timedOutRunIds?: string[], scope?: ReproducibilityScope): ReproducibilityReport;
|
|
76
|
+
export interface VerifyReproducibilityOptions extends VerifyOptions {
|
|
77
|
+
runs?: string;
|
|
78
|
+
interval?: string;
|
|
79
|
+
timeout?: string;
|
|
80
|
+
repoRoot?: string;
|
|
81
|
+
scope?: string;
|
|
82
|
+
planSource?: string;
|
|
83
|
+
}
|
|
84
|
+
export declare function runHasFrozenRiskPlan(run: VerificationRun): boolean;
|
|
85
|
+
export declare function verifyReproducibilityCommand(specimen: string, options: VerifyReproducibilityOptions): Promise<void>;
|