@haystackeditor/cli 0.15.28 → 0.15.30
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/dist/assets/telemetry/browser-runtime.js +167 -4
- package/dist/commands/precompute-delivery-contract.d.ts +68 -0
- package/dist/commands/precompute-delivery-contract.js +1 -0
- package/dist/commands/precompute-delivery-worker.d.ts +1 -0
- package/dist/commands/precompute-delivery-worker.js +68 -0
- package/dist/commands/precompute-delivery.d.ts +20 -0
- package/dist/commands/precompute-delivery.js +319 -0
- package/dist/commands/verify-explore.d.ts +14 -0
- package/dist/commands/verify-explore.js +180 -0
- package/dist/commands/verify-hosted-mcp.js +12 -2
- package/dist/commands/verify-hosted-reproducibility.d.ts +1 -0
- package/dist/commands/verify-hosted-reproducibility.js +4 -1
- package/dist/commands/verify-hosted.d.ts +18 -1
- package/dist/commands/verify-hosted.js +247 -51
- package/dist/commands/verify-precompute.d.ts +12 -1
- package/dist/commands/verify-precompute.js +137 -29
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.js +22 -0
- package/dist/index.js +49 -9
- package/dist/utils/design-verifier-result.d.ts +17 -0
- package/dist/utils/design-verifier-result.js +80 -1
- package/dist/utils/git.js +5 -1
- package/dist/utils/telemetry.js +8 -2
- package/package.json +1 -1
- package/schemas/cloud-verifier.v1.json +108 -45
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { withSchema } from '../schema.js';
|
|
4
5
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
5
|
-
import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
|
|
6
|
+
import { classifyHttpError, HaystackApiError, haystackApiUrl, haystackJson } from '../utils/haystack-api.js';
|
|
6
7
|
const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
7
8
|
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
8
9
|
const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
@@ -11,6 +12,12 @@ const STATUS = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
|
11
12
|
const TERMINAL_STATUSES = new Set(['complete', 'errored', 'expired', 'terminated']);
|
|
12
13
|
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
13
14
|
const DEFAULT_TIMEOUT_MINUTES = 35;
|
|
15
|
+
const INTENT_FIELDS = [
|
|
16
|
+
'problem',
|
|
17
|
+
'goal',
|
|
18
|
+
'intended_outcomes',
|
|
19
|
+
];
|
|
20
|
+
const FLEET_PROJECTION_VERSION = 'dv-cloud-verifier-fleet-runner-projection-v1';
|
|
14
21
|
function requireRecord(value, context) {
|
|
15
22
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
23
|
throw new Error(`${context} returned an invalid response object.`);
|
|
@@ -74,12 +81,67 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
74
81
|
? undefined
|
|
75
82
|
: requireRecord(outputValue, 'Hosted verifier output');
|
|
76
83
|
const resultValue = response.result;
|
|
77
|
-
const
|
|
84
|
+
const fleet = response.provider === 'haystack';
|
|
85
|
+
if (response.provider !== undefined && !fleet) {
|
|
86
|
+
throw new Error('Hosted verifier returned an unsupported provider.');
|
|
87
|
+
}
|
|
88
|
+
if (fleet && response.version !== FLEET_PROJECTION_VERSION) {
|
|
89
|
+
throw new Error('Hosted verifier returned an unsupported fleet projection version.');
|
|
90
|
+
}
|
|
91
|
+
const result = fleet || resultValue === undefined
|
|
78
92
|
? undefined
|
|
79
93
|
: parseHostedRunnerResult(resultValue);
|
|
80
94
|
const createdAt = optionalTimestamp(response, 'createdAt');
|
|
81
95
|
const completedAt = optionalTimestamp(response, 'completedAt');
|
|
82
|
-
|
|
96
|
+
let progress;
|
|
97
|
+
let fleetResult;
|
|
98
|
+
if (fleet) {
|
|
99
|
+
if (response.publicRunId !== runId || !repository || !baseCommit || !headCommit || !createdAt || output) {
|
|
100
|
+
throw new Error('Hosted fleet projection is missing its immutable run identity.');
|
|
101
|
+
}
|
|
102
|
+
if (response.progress !== undefined && response.progress !== null) {
|
|
103
|
+
progress = requireRecord(response.progress, 'Hosted fleet progress');
|
|
104
|
+
if (progress.version !== 'dv-verify-progress-v1' || progress.publicRunId !== runId) {
|
|
105
|
+
throw new Error('Hosted fleet progress does not match the requested run.');
|
|
106
|
+
}
|
|
107
|
+
requireString(progress, 'stage');
|
|
108
|
+
requireString(progress, 'status');
|
|
109
|
+
}
|
|
110
|
+
if (resultValue !== undefined && resultValue !== null) {
|
|
111
|
+
fleetResult = requireRecord(resultValue, 'Hosted fleet result');
|
|
112
|
+
const mapping = requireRecord(fleetResult.mapping, 'Hosted fleet result mapping');
|
|
113
|
+
const provenance = requireRecord(fleetResult.provenance, 'Hosted fleet provenance');
|
|
114
|
+
if (fleetResult.version !== 'dv-verify-result-v1' || fleetResult.provider !== 'haystack'
|
|
115
|
+
|| mapping.publicRunId !== runId || provenance.baseSha !== baseCommit || provenance.headSha !== headCommit) {
|
|
116
|
+
throw new Error('Hosted fleet result does not match the requested run and commits.');
|
|
117
|
+
}
|
|
118
|
+
// CONTRACT-GAP: P0 currently couples its canonical parsers to runner-only
|
|
119
|
+
// modules outside this published CLI. The server validates the complete
|
|
120
|
+
// P0 result; the CLI only checks the fields it presents, retaining the
|
|
121
|
+
// original document instead of defining a second P0 wire contract.
|
|
122
|
+
requireString(fleetResult, 'lifecycle', /^(complete|failed|cancelled|budget-stopped|account-action)$/);
|
|
123
|
+
requireString(fleetResult, 'runHealth', /^(healthy|degraded|infrastructure-error|not-executed)$/);
|
|
124
|
+
const cleanup = requireRecord(fleetResult.cleanup, 'Hosted fleet cleanup');
|
|
125
|
+
requireString(cleanup, 'status', /^(complete|pending)$/);
|
|
126
|
+
if (!Array.isArray(cleanup.pending) || !Array.isArray(fleetResult.findings)) {
|
|
127
|
+
throw new Error('Hosted fleet result has invalid findings or cleanup.');
|
|
128
|
+
}
|
|
129
|
+
for (const value of fleetResult.findings) {
|
|
130
|
+
const finding = requireRecord(value, 'Hosted fleet finding');
|
|
131
|
+
requireString(finding, 'findingId');
|
|
132
|
+
requireString(finding, 'caseId');
|
|
133
|
+
requireString(finding, 'judgment');
|
|
134
|
+
}
|
|
135
|
+
requireString(provenance, 'intent', /^(user|inferred)$/);
|
|
136
|
+
if (status === 'complete' && fleetResult.lifecycle !== 'complete') {
|
|
137
|
+
throw new Error('Hosted fleet lifecycle disagrees with its completed job.');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (status === 'complete' && !fleetResult) {
|
|
141
|
+
throw new Error('Hosted fleet run completed without a result receipt.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (status === 'complete') {
|
|
83
145
|
// Two terminal shapes exist: the Workflow receipt envelope (`output`)
|
|
84
146
|
// and the runner-queue result envelope (`result`). Exactly one must
|
|
85
147
|
// validate.
|
|
@@ -113,6 +175,7 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
113
175
|
completedAt,
|
|
114
176
|
output,
|
|
115
177
|
result,
|
|
178
|
+
...(fleet ? { provider: 'haystack', version: FLEET_PROJECTION_VERSION, progress, fleetResult } : {}),
|
|
116
179
|
error: optionalString(response, 'error'),
|
|
117
180
|
};
|
|
118
181
|
}
|
|
@@ -159,11 +222,67 @@ function requireCommit(value, flag) {
|
|
|
159
222
|
}
|
|
160
223
|
return value;
|
|
161
224
|
}
|
|
162
|
-
|
|
225
|
+
function requireRevision(value, flag) {
|
|
226
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 1_024 || /[\u0000-\u0020\u007f]/.test(value)) {
|
|
227
|
+
throw new Error(`${flag} must be a non-empty Git revision of at most 1024 characters without whitespace or control characters.`);
|
|
228
|
+
}
|
|
229
|
+
return value;
|
|
230
|
+
}
|
|
231
|
+
export function readHostedIntentFile(intentFile) {
|
|
232
|
+
if (typeof intentFile !== 'string' || intentFile.length === 0) {
|
|
233
|
+
throw new Error('--intent-file is required and must name a JSON file containing exactly problem, goal, and intended_outcomes.');
|
|
234
|
+
}
|
|
235
|
+
let source;
|
|
236
|
+
try {
|
|
237
|
+
source = readFileSync(intentFile, 'utf8');
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
241
|
+
throw new Error(`Could not read --intent-file "${intentFile}": ${reason}`);
|
|
242
|
+
}
|
|
243
|
+
let parsed;
|
|
244
|
+
try {
|
|
245
|
+
parsed = JSON.parse(source);
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
throw new Error(`--intent-file "${intentFile}" must contain valid JSON.`);
|
|
249
|
+
}
|
|
250
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
251
|
+
throw new Error(`--intent-file "${intentFile}" must contain exactly problem, goal, and intended_outcomes.`);
|
|
252
|
+
}
|
|
253
|
+
const record = parsed;
|
|
254
|
+
const unknownField = Object.keys(record)
|
|
255
|
+
.find(field => !INTENT_FIELDS.includes(field));
|
|
256
|
+
if (unknownField) {
|
|
257
|
+
throw new Error(`--intent-file "${intentFile}" contains unknown field "${unknownField}"; expected exactly problem, goal, and intended_outcomes.`);
|
|
258
|
+
}
|
|
259
|
+
for (const field of ['problem', 'goal']) {
|
|
260
|
+
if (typeof record[field] !== 'string' || record[field].trim().length === 0) {
|
|
261
|
+
throw new Error(`--intent-file "${intentFile}" field "${field}" must be a non-empty string.`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const outcomes = record.intended_outcomes;
|
|
265
|
+
if (!Array.isArray(outcomes)
|
|
266
|
+
|| outcomes.length === 0
|
|
267
|
+
|| outcomes.some(item => typeof item !== 'string' || item.trim().length === 0)) {
|
|
268
|
+
throw new Error(`--intent-file "${intentFile}" field "intended_outcomes" must be a non-empty array of non-empty strings.`);
|
|
269
|
+
}
|
|
270
|
+
if (Object.keys(record).length !== INTENT_FIELDS.length) {
|
|
271
|
+
throw new Error(`--intent-file "${intentFile}" must contain exactly problem, goal, and intended_outcomes.`);
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
problem: record.problem,
|
|
275
|
+
goal: record.goal,
|
|
276
|
+
intended_outcomes: record.intended_outcomes,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom, fleetRequest) {
|
|
163
280
|
const digest = createHash('sha256')
|
|
164
|
-
.update(JSON.stringify(
|
|
165
|
-
? [repository, baseCommit, headCommit,
|
|
166
|
-
:
|
|
281
|
+
.update(JSON.stringify(fleetRequest
|
|
282
|
+
? [fleetRequest.provider, repository, baseCommit, headCommit, fleetRequest.intent ?? null]
|
|
283
|
+
: replayPlanFrom
|
|
284
|
+
? [repository, baseCommit, headCommit, replayPlanFrom]
|
|
285
|
+
: [repository, baseCommit, headCommit]))
|
|
167
286
|
.digest('hex');
|
|
168
287
|
return `cli:${digest}`;
|
|
169
288
|
}
|
|
@@ -190,40 +309,78 @@ export function hostedPollingOptions(interval, timeout, onStatus) {
|
|
|
190
309
|
};
|
|
191
310
|
}
|
|
192
311
|
export async function startHostedVerification(repositoryArg, options, token) {
|
|
193
|
-
const repository =
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const replayPlanFrom = options.replayPlanFrom;
|
|
197
|
-
if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
|
|
198
|
-
throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
|
|
199
|
-
}
|
|
200
|
-
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
201
|
-
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom));
|
|
202
|
-
const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
|
|
312
|
+
const { repository, launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
|
|
313
|
+
const path = '/api/agent/cloud-verifier/runs';
|
|
314
|
+
const response = await fetch(haystackApiUrl(path), {
|
|
203
315
|
method: 'POST',
|
|
204
|
-
headers: {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
...(replayPlanFrom
|
|
212
|
-
? { replayPlanFromRunId: replayPlanFrom }
|
|
213
|
-
: {}),
|
|
214
|
-
}),
|
|
316
|
+
headers: {
|
|
317
|
+
Authorization: `Bearer ${token}`,
|
|
318
|
+
Accept: 'application/json',
|
|
319
|
+
'User-Agent': 'Haystack-CLI',
|
|
320
|
+
'Content-Type': 'application/json',
|
|
321
|
+
},
|
|
322
|
+
body: JSON.stringify(launchBody),
|
|
215
323
|
});
|
|
216
|
-
|
|
324
|
+
if (!response.ok) {
|
|
325
|
+
// Keep the shared HTTP classification and raw-body fallback while exposing
|
|
326
|
+
// the launch service's additional explanation for a retryable failure.
|
|
327
|
+
const body = response.status === 503
|
|
328
|
+
? await response.clone().json().catch(() => null)
|
|
329
|
+
: null;
|
|
330
|
+
const error = await classifyHttpError(response, `Haystack API ${path}`);
|
|
331
|
+
if (body && typeof body === 'object' && 'detail' in body
|
|
332
|
+
&& typeof body.detail === 'string' && body.detail.trim()) {
|
|
333
|
+
error.message += ` Details: ${body.detail}`;
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
const state = parseHostedRunState(await response.json());
|
|
338
|
+
if (options.provider === 'haystack' && state.provider !== 'haystack') {
|
|
339
|
+
throw new Error('Hosted verifier did not acknowledge the fleet-pinned request.');
|
|
340
|
+
}
|
|
217
341
|
return {
|
|
218
342
|
state: {
|
|
219
343
|
...state,
|
|
220
344
|
repository: state.repository ?? repository.fullName,
|
|
221
|
-
baseCommit: state.baseCommit ?? baseCommit,
|
|
222
|
-
headCommit: state.headCommit ?? headCommit,
|
|
345
|
+
baseCommit: state.baseCommit ?? launchBody.baseCommit,
|
|
346
|
+
headCommit: state.headCommit ?? launchBody.headCommit,
|
|
223
347
|
},
|
|
224
348
|
idempotencyKey,
|
|
225
349
|
};
|
|
226
350
|
}
|
|
351
|
+
function prepareHostedLaunch(repositoryArg, options) {
|
|
352
|
+
const repository = parseHostedRepository(repositoryArg);
|
|
353
|
+
const fleet = options.provider === 'haystack';
|
|
354
|
+
const validateRevision = fleet ? requireRevision : requireCommit;
|
|
355
|
+
const baseCommit = validateRevision(options.base, '--base');
|
|
356
|
+
const headCommit = validateRevision(options.head, '--head');
|
|
357
|
+
const intent = fleet && options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
|
|
358
|
+
if (fleet && intent && [intent.problem, intent.goal, ...intent.intended_outcomes].some(value => /[\u0000-\u001f\u007f]/.test(value))) {
|
|
359
|
+
throw new Error('Fleet --intent-file fields must be single-line text without control characters.');
|
|
360
|
+
}
|
|
361
|
+
const replayPlanFrom = options.replayPlanFrom;
|
|
362
|
+
if (fleet && replayPlanFrom !== undefined) {
|
|
363
|
+
throw new Error('--replay-plan-from is available through the explicit hosted start alias.');
|
|
364
|
+
}
|
|
365
|
+
if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
|
|
366
|
+
throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
|
|
367
|
+
}
|
|
368
|
+
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
369
|
+
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom, fleet ? { provider: 'haystack', intent } : undefined));
|
|
370
|
+
const launchBody = {
|
|
371
|
+
...(fleet ? { provider: 'haystack' } : {}),
|
|
372
|
+
owner: repository.owner,
|
|
373
|
+
repository: repository.repository,
|
|
374
|
+
baseCommit,
|
|
375
|
+
headCommit,
|
|
376
|
+
idempotencyKey,
|
|
377
|
+
...(intent ? { intent } : {}),
|
|
378
|
+
...(replayPlanFrom
|
|
379
|
+
? { replayPlanFromRunId: replayPlanFrom }
|
|
380
|
+
: {}),
|
|
381
|
+
};
|
|
382
|
+
return { repository, launchBody, idempotencyKey };
|
|
383
|
+
}
|
|
227
384
|
export async function getHostedVerification(runId, token) {
|
|
228
385
|
if (!RUN_ID.test(runId)) {
|
|
229
386
|
throw new Error('Run id must match cv_ followed by 48 lowercase hex characters.');
|
|
@@ -246,12 +403,13 @@ function delay(milliseconds) {
|
|
|
246
403
|
}
|
|
247
404
|
export async function waitForHostedVerification(runId, token, options) {
|
|
248
405
|
const startedAt = Date.now();
|
|
249
|
-
let
|
|
406
|
+
let lastProgress;
|
|
250
407
|
for (;;) {
|
|
251
408
|
const state = await getHostedVerification(runId, token);
|
|
252
|
-
|
|
409
|
+
const progressKey = JSON.stringify([state.status, state.progress?.sequence, state.progress?.stage, state.progress?.status]);
|
|
410
|
+
if (progressKey !== lastProgress) {
|
|
253
411
|
options.onStatus?.(state);
|
|
254
|
-
|
|
412
|
+
lastProgress = progressKey;
|
|
255
413
|
}
|
|
256
414
|
if (TERMINAL_STATUSES.has(state.status)) {
|
|
257
415
|
return { state, timedOut: false };
|
|
@@ -281,6 +439,9 @@ export function buildHostedJson(operation, state, extras = {}) {
|
|
|
281
439
|
...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
|
|
282
440
|
...(state.output ? { output: state.output } : {}),
|
|
283
441
|
...(state.result ? { result: state.result } : {}),
|
|
442
|
+
...(state.provider ? { provider: state.provider, version: state.version } : {}),
|
|
443
|
+
...(state.progress ? { progress: state.progress } : {}),
|
|
444
|
+
...(state.fleetResult ? { result: state.fleetResult } : {}),
|
|
284
445
|
...(state.error ? { error: state.error } : {}),
|
|
285
446
|
});
|
|
286
447
|
}
|
|
@@ -300,11 +461,13 @@ function printHostedState(operation, state, options) {
|
|
|
300
461
|
}), null, 2)}\n`);
|
|
301
462
|
return;
|
|
302
463
|
}
|
|
303
|
-
const icon = state.status === 'complete'
|
|
304
|
-
? chalk.
|
|
305
|
-
:
|
|
306
|
-
? chalk.
|
|
307
|
-
:
|
|
464
|
+
const icon = state.provider === 'haystack' && state.status === 'complete'
|
|
465
|
+
? chalk.cyan('•')
|
|
466
|
+
: state.status === 'complete'
|
|
467
|
+
? chalk.green('✓')
|
|
468
|
+
: TERMINAL_STATUSES.has(state.status)
|
|
469
|
+
? chalk.red('✗')
|
|
470
|
+
: chalk.cyan('•');
|
|
308
471
|
console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
|
|
309
472
|
if (state.repository) {
|
|
310
473
|
console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
|
|
@@ -321,6 +484,22 @@ function printHostedState(operation, state, options) {
|
|
|
321
484
|
}
|
|
322
485
|
console.log(` ${chalk.dim('Artifacts:')} ${state.result.outputLocation}`);
|
|
323
486
|
}
|
|
487
|
+
if (state.progress) {
|
|
488
|
+
console.log(` ${chalk.dim('Progress:')} ${state.progress.stage} (${state.progress.status})`);
|
|
489
|
+
}
|
|
490
|
+
if (state.fleetResult) {
|
|
491
|
+
const result = state.fleetResult;
|
|
492
|
+
const cleanup = requireRecord(result.cleanup, 'Hosted fleet cleanup');
|
|
493
|
+
const provenance = requireRecord(result.provenance, 'Hosted fleet provenance');
|
|
494
|
+
console.log(` ${chalk.dim('Lifecycle:')} ${result.lifecycle}`);
|
|
495
|
+
console.log(` ${chalk.dim('Run health:')} ${result.runHealth}`);
|
|
496
|
+
console.log(` ${chalk.dim('Intent:')} ${provenance.intent}`);
|
|
497
|
+
console.log(` ${chalk.dim('Cleanup:')} ${cleanup.status} (${cleanup.pending.length} pending)`);
|
|
498
|
+
for (const value of result.findings) {
|
|
499
|
+
const finding = requireRecord(value, 'Hosted fleet finding');
|
|
500
|
+
console.log(` ${chalk.dim('Finding:')} ${finding.findingId} / ${finding.caseId}: ${finding.judgment}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
324
503
|
if (state.error)
|
|
325
504
|
console.log(` ${chalk.dim('Error:')} ${state.error}`);
|
|
326
505
|
if (options.timedOut) {
|
|
@@ -330,20 +509,38 @@ function printHostedState(operation, state, options) {
|
|
|
330
509
|
console.log(` ${chalk.dim('Resume:')} ${nextCommand}`);
|
|
331
510
|
}
|
|
332
511
|
function markNonSuccess(state, timedOut = false) {
|
|
512
|
+
if (state.fleetResult) {
|
|
513
|
+
const result = state.fleetResult;
|
|
514
|
+
const cleanup = requireRecord(result.cleanup, 'Hosted fleet cleanup');
|
|
515
|
+
if (result.lifecycle !== 'complete' || result.runHealth !== 'healthy' || cleanup.status !== 'complete') {
|
|
516
|
+
process.exitCode = 2;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (result.findings.some(value => requireRecord(value, 'Hosted fleet finding').judgment === 'regression')) {
|
|
520
|
+
process.exitCode = 1;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
333
523
|
if (timedOut
|
|
334
524
|
|| (state.status !== 'complete' && TERMINAL_STATUSES.has(state.status))) {
|
|
335
525
|
process.exitCode = 2;
|
|
336
526
|
}
|
|
337
527
|
}
|
|
338
528
|
export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
339
|
-
|
|
529
|
+
// Validate all local input before resolving accounts or submitting a job.
|
|
530
|
+
const { repository } = prepareHostedLaunch(repositoryArg, options);
|
|
531
|
+
const polling = hostedPollingOptions(options.interval, options.timeout, current => {
|
|
532
|
+
if (!options.json)
|
|
533
|
+
console.error(chalk.dim(hostedProgressMessage(current)));
|
|
534
|
+
});
|
|
340
535
|
const auth = await resolveAuthContext({
|
|
341
536
|
preferredLogin: options.account,
|
|
342
537
|
owner: repository.owner,
|
|
343
538
|
repo: repository.repository,
|
|
344
539
|
});
|
|
345
540
|
if (!options.json) {
|
|
346
|
-
console.error(chalk.dim(
|
|
541
|
+
console.error(chalk.dim(options.provider === 'haystack'
|
|
542
|
+
? 'Submitting repository revisions to the hosted fleet verifier…'
|
|
543
|
+
: 'Submitting the exact commit pair to the hosted verifier…'));
|
|
347
544
|
}
|
|
348
545
|
const started = await startHostedVerification(repositoryArg, options, auth.token);
|
|
349
546
|
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
@@ -352,11 +549,7 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
|
352
549
|
let state = started.state;
|
|
353
550
|
let timedOut;
|
|
354
551
|
if (options.wait !== false && !TERMINAL_STATUSES.has(state.status)) {
|
|
355
|
-
const waited = await waitForHostedVerification(state.runId, auth.token,
|
|
356
|
-
if (!options.json) {
|
|
357
|
-
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
358
|
-
}
|
|
359
|
-
}));
|
|
552
|
+
const waited = await waitForHostedVerification(state.runId, auth.token, polling);
|
|
360
553
|
state = waited.state;
|
|
361
554
|
timedOut = waited.timedOut;
|
|
362
555
|
}
|
|
@@ -371,7 +564,14 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
|
371
564
|
});
|
|
372
565
|
markNonSuccess(state, timedOut);
|
|
373
566
|
}
|
|
567
|
+
function hostedProgressMessage(state) {
|
|
568
|
+
return `Hosted verifier: ${state.status}${state.progress ? ` · ${state.progress.stage} (${state.progress.status})` : ''}…`;
|
|
569
|
+
}
|
|
374
570
|
export async function verifyHostedStatusCommand(runId, options) {
|
|
571
|
+
const polling = hostedPollingOptions(options.interval, options.timeout, current => {
|
|
572
|
+
if (!options.json)
|
|
573
|
+
console.error(chalk.dim(hostedProgressMessage(current)));
|
|
574
|
+
});
|
|
375
575
|
const auth = await resolveAuthContext({ preferredLogin: options.account });
|
|
376
576
|
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
377
577
|
? undefined
|
|
@@ -379,11 +579,7 @@ export async function verifyHostedStatusCommand(runId, options) {
|
|
|
379
579
|
let state = await getHostedVerification(runId, auth.token);
|
|
380
580
|
let timedOut;
|
|
381
581
|
if (options.wait && !TERMINAL_STATUSES.has(state.status)) {
|
|
382
|
-
const waited = await waitForHostedVerification(runId, auth.token,
|
|
383
|
-
if (!options.json) {
|
|
384
|
-
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
385
|
-
}
|
|
386
|
-
}));
|
|
582
|
+
const waited = await waitForHostedVerification(runId, auth.token, polling);
|
|
387
583
|
state = waited.state;
|
|
388
584
|
timedOut = waited.timedOut;
|
|
389
585
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import type { DeliverPrecomputeRequest } from './precompute-delivery-contract.js';
|
|
2
|
+
export type RepoWalkHookStatus = {
|
|
3
|
+
status: 'queued' | 'existing';
|
|
4
|
+
runId: string;
|
|
5
|
+
} | {
|
|
6
|
+
status: 'unavailable';
|
|
7
|
+
reason: 'runner-unavailable' | 'enqueue-failed';
|
|
8
|
+
};
|
|
9
|
+
export type PrecomputeOutcome = 'pending' | 'enqueued' | 'cached' | 'existing' | 'skipped_no_auth' | 'error';
|
|
2
10
|
export interface VerifyPrecomputeOptions {
|
|
3
11
|
hook?: boolean;
|
|
4
12
|
}
|
|
@@ -7,6 +15,7 @@ export interface FrontHalfEnqueueRequest {
|
|
|
7
15
|
repository: string;
|
|
8
16
|
baseCommit: string;
|
|
9
17
|
workCommit: string;
|
|
18
|
+
observedAt: number;
|
|
10
19
|
cacheKey: string;
|
|
11
20
|
patchGzBase64: string;
|
|
12
21
|
patchSha256: string;
|
|
@@ -16,4 +25,6 @@ export interface PrecomputeDerivation {
|
|
|
16
25
|
request: FrontHalfEnqueueRequest;
|
|
17
26
|
}
|
|
18
27
|
export declare function derivePrecomputeRequest(gitRoot: string, owner: string, repository: string, deadline?: number): Promise<PrecomputeDerivation>;
|
|
28
|
+
/** One authenticated submission of the already captured request. No source reads or retries. */
|
|
29
|
+
export declare const deliverPrecomputeRequest: DeliverPrecomputeRequest;
|
|
19
30
|
export declare function verifyPrecomputeCommand(options: VerifyPrecomputeOptions): Promise<void>;
|