@haystackeditor/cli 0.15.29 → 0.15.31
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 +148 -2
- package/dist/commands/design-verify.d.ts +2 -0
- package/dist/commands/design-verify.js +27 -1
- 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-history.d.ts +14 -0
- package/dist/commands/verify-history.js +152 -0
- package/dist/commands/verify-hosted.d.ts +12 -2
- package/dist/commands/verify-hosted.js +305 -50
- package/dist/commands/verify-precompute.d.ts +12 -1
- package/dist/commands/verify-precompute.js +135 -27
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.js +22 -0
- package/dist/index.js +71 -5
- package/dist/types/verify-history.d.ts +38 -0
- package/dist/types/verify-history.js +1 -0
- package/dist/utils/design-verifier-api.d.ts +8 -0
- package/dist/utils/design-verifier-api.js +23 -0
- package/dist/utils/design-verifier-history.d.ts +46 -0
- package/dist/utils/design-verifier-history.js +79 -0
- 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
|
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { withSchema } from '../schema.js';
|
|
5
5
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
6
|
-
import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
|
|
6
|
+
import { classifyHttpError, HaystackApiError, haystackApiUrl, haystackJson } from '../utils/haystack-api.js';
|
|
7
7
|
const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
8
8
|
const COMMIT_SHA = /^[0-9a-f]{40}$/;
|
|
9
9
|
const IDEMPOTENCY_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
|
|
@@ -17,6 +17,7 @@ const INTENT_FIELDS = [
|
|
|
17
17
|
'goal',
|
|
18
18
|
'intended_outcomes',
|
|
19
19
|
];
|
|
20
|
+
const FLEET_PROJECTION_VERSION = 'dv-cloud-verifier-fleet-runner-projection-v1';
|
|
20
21
|
function requireRecord(value, context) {
|
|
21
22
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
22
23
|
throw new Error(`${context} returned an invalid response object.`);
|
|
@@ -80,12 +81,70 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
80
81
|
? undefined
|
|
81
82
|
: requireRecord(outputValue, 'Hosted verifier output');
|
|
82
83
|
const resultValue = response.result;
|
|
83
|
-
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
|
|
84
92
|
? undefined
|
|
85
93
|
: parseHostedRunnerResult(resultValue);
|
|
86
94
|
const createdAt = optionalTimestamp(response, 'createdAt');
|
|
87
95
|
const completedAt = optionalTimestamp(response, 'completedAt');
|
|
88
|
-
|
|
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
|
+
if (fleetResult.savedChecks !== undefined) {
|
|
136
|
+
parseHostedSavedChecks(fleetResult.savedChecks, runId, headCommit);
|
|
137
|
+
}
|
|
138
|
+
requireString(provenance, 'intent', /^(user|inferred)$/);
|
|
139
|
+
if (status === 'complete' && fleetResult.lifecycle !== 'complete') {
|
|
140
|
+
throw new Error('Hosted fleet lifecycle disagrees with its completed job.');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (status === 'complete' && !fleetResult) {
|
|
144
|
+
throw new Error('Hosted fleet run completed without a result receipt.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
else if (status === 'complete') {
|
|
89
148
|
// Two terminal shapes exist: the Workflow receipt envelope (`output`)
|
|
90
149
|
// and the runner-queue result envelope (`result`). Exactly one must
|
|
91
150
|
// validate.
|
|
@@ -119,6 +178,7 @@ export function parseHostedRunState(value, expectedRunId) {
|
|
|
119
178
|
completedAt,
|
|
120
179
|
output,
|
|
121
180
|
result,
|
|
181
|
+
...(fleet ? { provider: 'haystack', version: FLEET_PROJECTION_VERSION, progress, fleetResult } : {}),
|
|
122
182
|
error: optionalString(response, 'error'),
|
|
123
183
|
};
|
|
124
184
|
}
|
|
@@ -159,12 +219,104 @@ export function parseHostedRepository(value) {
|
|
|
159
219
|
fullName: value,
|
|
160
220
|
};
|
|
161
221
|
}
|
|
222
|
+
/** The service validates the shared full contract. Check every additional
|
|
223
|
+
* field used by this published CLI before displaying or accepting a clean run. */
|
|
224
|
+
function parseHostedSavedChecks(value, runId, headSha) {
|
|
225
|
+
const saved = requireRecord(value, 'Hosted saved checks');
|
|
226
|
+
if (saved.version !== 'dv-fleet-saved-check-result-v1'
|
|
227
|
+
|| !Array.isArray(saved.descriptors) || !Array.isArray(saved.outcomes)
|
|
228
|
+
|| !Array.isArray(saved.unresolvedFindings) || !Array.isArray(saved.conflicts)) {
|
|
229
|
+
throw new Error('Hosted saved checks have an invalid version or records.');
|
|
230
|
+
}
|
|
231
|
+
const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
|
|
232
|
+
requireString(completeness, 'status', /^(complete|incomplete)$/);
|
|
233
|
+
if (completeness.status === 'incomplete' && (!Array.isArray(completeness.reasons) || completeness.reasons.length === 0)) {
|
|
234
|
+
throw new Error('Hosted saved check history has no explanation for missing evidence.');
|
|
235
|
+
}
|
|
236
|
+
const ref = (record) => {
|
|
237
|
+
requireString(record, 'checkId', /^[0-9a-f]{64}$/);
|
|
238
|
+
requireString(record, 'planRevisionId', /^[0-9a-f]{64}$/);
|
|
239
|
+
return `${record.checkId}:${record.planRevisionId}`;
|
|
240
|
+
};
|
|
241
|
+
const descriptors = new Map();
|
|
242
|
+
for (const value of saved.descriptors) {
|
|
243
|
+
const descriptor = requireRecord(value, 'Hosted saved check descriptor');
|
|
244
|
+
const key = ref(descriptor);
|
|
245
|
+
if (descriptor.version !== 'dv-fleet-saved-check-v1' || descriptors.has(key)) {
|
|
246
|
+
throw new Error('Hosted saved check descriptor identity is invalid or duplicated.');
|
|
247
|
+
}
|
|
248
|
+
requireString(descriptor, 'captureStatus', /^(captured|unavailable)$/);
|
|
249
|
+
descriptors.set(key, descriptor);
|
|
250
|
+
}
|
|
251
|
+
const outcomes = new Map();
|
|
252
|
+
for (const value of saved.outcomes) {
|
|
253
|
+
const outcome = requireRecord(value, 'Hosted saved check outcome');
|
|
254
|
+
const key = ref(outcome);
|
|
255
|
+
requireString(outcome, 'status', /^(passed|reproduced|blocked|inconclusive)$/);
|
|
256
|
+
if (outcome.version !== 'dv-fleet-saved-check-outcome-v1'
|
|
257
|
+
|| (!descriptors.has(key) && completeness.status !== 'incomplete') || outcomes.has(key)
|
|
258
|
+
|| outcome.publicRunId !== runId || outcome.headSha !== headSha
|
|
259
|
+
|| (outcome.currentCaseId !== null && typeof outcome.currentCaseId !== 'string')
|
|
260
|
+
|| (outcome.blockedCode !== null && typeof outcome.blockedCode !== 'string')) {
|
|
261
|
+
throw new Error('Hosted saved check outcome does not match its current run or original check.');
|
|
262
|
+
}
|
|
263
|
+
if ((outcome.status === 'blocked') !== (outcome.blockedCode !== null)
|
|
264
|
+
|| (outcome.status === 'passed' && (descriptors.get(key)?.captureStatus !== 'captured'
|
|
265
|
+
|| outcome.currentCaseId === null || !outcome.receipt || completeness.status !== 'complete'))) {
|
|
266
|
+
throw new Error('Hosted saved check outcome claims a pass without original execution evidence.');
|
|
267
|
+
}
|
|
268
|
+
outcomes.set(key, outcome);
|
|
269
|
+
}
|
|
270
|
+
const findings = new Set();
|
|
271
|
+
for (const value of saved.unresolvedFindings) {
|
|
272
|
+
const finding = requireRecord(value, 'Hosted unresolved finding');
|
|
273
|
+
const key = ref(finding);
|
|
274
|
+
requireString(finding, 'findingId');
|
|
275
|
+
const source = requireRecord(finding.firstObserved, 'Hosted original finding evidence');
|
|
276
|
+
requireString(source, 'publicRunId', RUN_ID);
|
|
277
|
+
requireString(source, 'caseId');
|
|
278
|
+
requireString(source, 'headSha', COMMIT_SHA);
|
|
279
|
+
if (finding.version !== 'dv-fleet-unresolved-finding-v1'
|
|
280
|
+
|| (!descriptors.has(key) && completeness.status !== 'incomplete')
|
|
281
|
+
|| findings.has(finding.findingId) || !Array.isArray(source.evidence) || source.evidence.length === 0
|
|
282
|
+
|| outcomes.get(key)?.status === 'passed') {
|
|
283
|
+
throw new Error('Hosted unresolved finding has invalid original evidence or check identity.');
|
|
284
|
+
}
|
|
285
|
+
findings.add(finding.findingId);
|
|
286
|
+
if (finding.lastOutcome !== null) {
|
|
287
|
+
const last = requireRecord(finding.lastOutcome, 'Hosted unresolved finding outcome');
|
|
288
|
+
if (ref(last) !== key || last.status === 'passed')
|
|
289
|
+
throw new Error('Hosted unresolved finding lost its exact original check.');
|
|
290
|
+
requireString(last, 'status', /^(reproduced|blocked|inconclusive)$/);
|
|
291
|
+
if (last.blockedCode !== null)
|
|
292
|
+
requireString(last, 'blockedCode');
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const value of saved.conflicts) {
|
|
296
|
+
const conflict = requireRecord(value, 'Hosted saved check conflict');
|
|
297
|
+
requireString(conflict, 'checkId', /^[0-9a-f]{64}$/);
|
|
298
|
+
if (!Array.isArray(conflict.planRevisionIds) || conflict.planRevisionIds.length < 2
|
|
299
|
+
|| conflict.planRevisionIds.some(id => typeof id !== 'string' || !/^[0-9a-f]{64}$/.test(id))) {
|
|
300
|
+
throw new Error('Hosted saved check conflict is invalid.');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return saved;
|
|
304
|
+
}
|
|
305
|
+
function savedCheckDisplay(value) {
|
|
306
|
+
return String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
|
|
307
|
+
}
|
|
162
308
|
function requireCommit(value, flag) {
|
|
163
309
|
if (typeof value !== 'string' || !COMMIT_SHA.test(value)) {
|
|
164
310
|
throw new Error(`${flag} must be an exact 40-character lowercase commit SHA.`);
|
|
165
311
|
}
|
|
166
312
|
return value;
|
|
167
313
|
}
|
|
314
|
+
function requireRevision(value, flag) {
|
|
315
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 1_024 || /[\u0000-\u0020\u007f]/.test(value)) {
|
|
316
|
+
throw new Error(`${flag} must be a non-empty Git revision of at most 1024 characters without whitespace or control characters.`);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
168
320
|
export function readHostedIntentFile(intentFile) {
|
|
169
321
|
if (typeof intentFile !== 'string' || intentFile.length === 0) {
|
|
170
322
|
throw new Error('--intent-file is required and must name a JSON file containing exactly problem, goal, and intended_outcomes.');
|
|
@@ -213,11 +365,13 @@ export function readHostedIntentFile(intentFile) {
|
|
|
213
365
|
intended_outcomes: record.intended_outcomes,
|
|
214
366
|
};
|
|
215
367
|
}
|
|
216
|
-
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom) {
|
|
368
|
+
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom, fleetRequest) {
|
|
217
369
|
const digest = createHash('sha256')
|
|
218
|
-
.update(JSON.stringify(
|
|
219
|
-
? [repository, baseCommit, headCommit,
|
|
220
|
-
:
|
|
370
|
+
.update(JSON.stringify(fleetRequest
|
|
371
|
+
? [fleetRequest.provider, repository, baseCommit, headCommit, fleetRequest.intent ?? null]
|
|
372
|
+
: replayPlanFrom
|
|
373
|
+
? [repository, baseCommit, headCommit, replayPlanFrom]
|
|
374
|
+
: [repository, baseCommit, headCommit]))
|
|
221
375
|
.digest('hex');
|
|
222
376
|
return `cli:${digest}`;
|
|
223
377
|
}
|
|
@@ -244,42 +398,77 @@ export function hostedPollingOptions(interval, timeout, onStatus) {
|
|
|
244
398
|
};
|
|
245
399
|
}
|
|
246
400
|
export async function startHostedVerification(repositoryArg, options, token) {
|
|
401
|
+
const { repository, launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
|
|
402
|
+
const path = '/api/agent/cloud-verifier/runs';
|
|
403
|
+
const response = await fetch(haystackApiUrl(path), {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: {
|
|
406
|
+
Authorization: `Bearer ${token}`,
|
|
407
|
+
Accept: 'application/json',
|
|
408
|
+
'User-Agent': 'Haystack-CLI',
|
|
409
|
+
'Content-Type': 'application/json',
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify(launchBody),
|
|
412
|
+
});
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
// Keep the shared HTTP classification and raw-body fallback while exposing
|
|
415
|
+
// the launch service's additional explanation for a retryable failure.
|
|
416
|
+
const body = response.status === 503
|
|
417
|
+
? await response.clone().json().catch(() => null)
|
|
418
|
+
: null;
|
|
419
|
+
const error = await classifyHttpError(response, `Haystack API ${path}`);
|
|
420
|
+
if (body && typeof body === 'object' && 'detail' in body
|
|
421
|
+
&& typeof body.detail === 'string' && body.detail.trim()) {
|
|
422
|
+
error.message += ` Details: ${body.detail}`;
|
|
423
|
+
}
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
const state = parseHostedRunState(await response.json());
|
|
427
|
+
if (options.provider === 'haystack' && state.provider !== 'haystack') {
|
|
428
|
+
throw new Error('Hosted verifier did not acknowledge the fleet-pinned request.');
|
|
429
|
+
}
|
|
430
|
+
return {
|
|
431
|
+
state: {
|
|
432
|
+
...state,
|
|
433
|
+
repository: state.repository ?? repository.fullName,
|
|
434
|
+
baseCommit: state.baseCommit ?? launchBody.baseCommit,
|
|
435
|
+
headCommit: state.headCommit ?? launchBody.headCommit,
|
|
436
|
+
},
|
|
437
|
+
idempotencyKey,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function prepareHostedLaunch(repositoryArg, options) {
|
|
247
441
|
const repository = parseHostedRepository(repositoryArg);
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
const
|
|
442
|
+
const fleet = options.provider === 'haystack';
|
|
443
|
+
const validateRevision = fleet ? requireRevision : requireCommit;
|
|
444
|
+
const baseCommit = validateRevision(options.base, '--base');
|
|
445
|
+
const headCommit = validateRevision(options.head, '--head');
|
|
446
|
+
const intent = fleet && options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
|
|
447
|
+
if (fleet && intent && [intent.problem, intent.goal, ...intent.intended_outcomes].some(value => /[\u0000-\u001f\u007f]/.test(value))) {
|
|
448
|
+
throw new Error('Fleet --intent-file fields must be single-line text without control characters.');
|
|
449
|
+
}
|
|
251
450
|
const replayPlanFrom = options.replayPlanFrom;
|
|
451
|
+
if (fleet && replayPlanFrom !== undefined) {
|
|
452
|
+
throw new Error('--replay-plan-from is available through the explicit hosted start alias.');
|
|
453
|
+
}
|
|
252
454
|
if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
|
|
253
455
|
throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
|
|
254
456
|
}
|
|
255
457
|
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
256
|
-
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom));
|
|
458
|
+
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom, fleet ? { provider: 'haystack', intent } : undefined));
|
|
257
459
|
const launchBody = {
|
|
460
|
+
...(fleet ? { provider: 'haystack' } : {}),
|
|
258
461
|
owner: repository.owner,
|
|
259
462
|
repository: repository.repository,
|
|
260
463
|
baseCommit,
|
|
261
464
|
headCommit,
|
|
262
465
|
idempotencyKey,
|
|
263
|
-
intent,
|
|
466
|
+
...(intent ? { intent } : {}),
|
|
264
467
|
...(replayPlanFrom
|
|
265
468
|
? { replayPlanFromRunId: replayPlanFrom }
|
|
266
469
|
: {}),
|
|
267
470
|
};
|
|
268
|
-
|
|
269
|
-
method: 'POST',
|
|
270
|
-
headers: { 'Content-Type': 'application/json' },
|
|
271
|
-
body: JSON.stringify(launchBody),
|
|
272
|
-
});
|
|
273
|
-
const state = parseHostedRunState(response);
|
|
274
|
-
return {
|
|
275
|
-
state: {
|
|
276
|
-
...state,
|
|
277
|
-
repository: state.repository ?? repository.fullName,
|
|
278
|
-
baseCommit: state.baseCommit ?? baseCommit,
|
|
279
|
-
headCommit: state.headCommit ?? headCommit,
|
|
280
|
-
},
|
|
281
|
-
idempotencyKey,
|
|
282
|
-
};
|
|
471
|
+
return { repository, launchBody, idempotencyKey };
|
|
283
472
|
}
|
|
284
473
|
export async function getHostedVerification(runId, token) {
|
|
285
474
|
if (!RUN_ID.test(runId)) {
|
|
@@ -303,12 +492,13 @@ function delay(milliseconds) {
|
|
|
303
492
|
}
|
|
304
493
|
export async function waitForHostedVerification(runId, token, options) {
|
|
305
494
|
const startedAt = Date.now();
|
|
306
|
-
let
|
|
495
|
+
let lastProgress;
|
|
307
496
|
for (;;) {
|
|
308
497
|
const state = await getHostedVerification(runId, token);
|
|
309
|
-
|
|
498
|
+
const progressKey = JSON.stringify([state.status, state.progress?.sequence, state.progress?.stage, state.progress?.status]);
|
|
499
|
+
if (progressKey !== lastProgress) {
|
|
310
500
|
options.onStatus?.(state);
|
|
311
|
-
|
|
501
|
+
lastProgress = progressKey;
|
|
312
502
|
}
|
|
313
503
|
if (TERMINAL_STATUSES.has(state.status)) {
|
|
314
504
|
return { state, timedOut: false };
|
|
@@ -338,6 +528,9 @@ export function buildHostedJson(operation, state, extras = {}) {
|
|
|
338
528
|
...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
|
|
339
529
|
...(state.output ? { output: state.output } : {}),
|
|
340
530
|
...(state.result ? { result: state.result } : {}),
|
|
531
|
+
...(state.provider ? { provider: state.provider, version: state.version } : {}),
|
|
532
|
+
...(state.progress ? { progress: state.progress } : {}),
|
|
533
|
+
...(state.fleetResult ? { result: state.fleetResult } : {}),
|
|
341
534
|
...(state.error ? { error: state.error } : {}),
|
|
342
535
|
});
|
|
343
536
|
}
|
|
@@ -357,11 +550,13 @@ function printHostedState(operation, state, options) {
|
|
|
357
550
|
}), null, 2)}\n`);
|
|
358
551
|
return;
|
|
359
552
|
}
|
|
360
|
-
const icon = state.status === 'complete'
|
|
361
|
-
? chalk.
|
|
362
|
-
:
|
|
363
|
-
? chalk.
|
|
364
|
-
:
|
|
553
|
+
const icon = state.provider === 'haystack' && state.status === 'complete'
|
|
554
|
+
? chalk.cyan('•')
|
|
555
|
+
: state.status === 'complete'
|
|
556
|
+
? chalk.green('✓')
|
|
557
|
+
: TERMINAL_STATUSES.has(state.status)
|
|
558
|
+
? chalk.red('✗')
|
|
559
|
+
: chalk.cyan('•');
|
|
365
560
|
console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
|
|
366
561
|
if (state.repository) {
|
|
367
562
|
console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
|
|
@@ -378,6 +573,42 @@ function printHostedState(operation, state, options) {
|
|
|
378
573
|
}
|
|
379
574
|
console.log(` ${chalk.dim('Artifacts:')} ${state.result.outputLocation}`);
|
|
380
575
|
}
|
|
576
|
+
if (state.progress) {
|
|
577
|
+
console.log(` ${chalk.dim('Progress:')} ${state.progress.stage} (${state.progress.status})`);
|
|
578
|
+
}
|
|
579
|
+
if (state.fleetResult) {
|
|
580
|
+
const result = state.fleetResult;
|
|
581
|
+
const cleanup = requireRecord(result.cleanup, 'Hosted fleet cleanup');
|
|
582
|
+
const provenance = requireRecord(result.provenance, 'Hosted fleet provenance');
|
|
583
|
+
console.log(` ${chalk.dim('Lifecycle:')} ${result.lifecycle}`);
|
|
584
|
+
console.log(` ${chalk.dim('Run health:')} ${result.runHealth}`);
|
|
585
|
+
console.log(` ${chalk.dim('Intent:')} ${provenance.intent}`);
|
|
586
|
+
console.log(` ${chalk.dim('Cleanup:')} ${cleanup.status} (${cleanup.pending.length} pending)`);
|
|
587
|
+
for (const value of result.findings) {
|
|
588
|
+
const finding = requireRecord(value, 'Hosted fleet finding');
|
|
589
|
+
console.log(` ${chalk.dim('Finding:')} ${finding.findingId} / ${finding.caseId}: ${finding.judgment}`);
|
|
590
|
+
}
|
|
591
|
+
if (result.savedChecks !== undefined) {
|
|
592
|
+
const saved = requireRecord(result.savedChecks, 'Hosted saved checks');
|
|
593
|
+
const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
|
|
594
|
+
const unresolved = saved.unresolvedFindings;
|
|
595
|
+
console.log(` ${chalk.dim('Saved checks:')} ${completeness.status}; ${saved.conflicts.length} conflicts; ${unresolved.length} unresolved bugs`);
|
|
596
|
+
for (const value of unresolved) {
|
|
597
|
+
const finding = requireRecord(value, 'Hosted unresolved finding');
|
|
598
|
+
const source = requireRecord(finding.firstObserved, 'Hosted original finding evidence');
|
|
599
|
+
const last = finding.lastOutcome === null ? null : requireRecord(finding.lastOutcome, 'Hosted unresolved finding outcome');
|
|
600
|
+
console.log(` ${chalk.yellow('Unresolved:')} ${savedCheckDisplay(finding.findingId)} / ${savedCheckDisplay(source.caseId)}`);
|
|
601
|
+
console.log(` First reproduced at ${String(source.headSha).slice(0, 12)}; latest check: ${savedCheckDisplay(last?.status ?? 'not executed')}${last?.blockedCode ? ` (${savedCheckDisplay(last.blockedCode)})` : ''}`);
|
|
602
|
+
}
|
|
603
|
+
if (unresolved.length)
|
|
604
|
+
console.log(' Earlier bug evidence remains open until the exact saved check passes.');
|
|
605
|
+
if (completeness.status === 'incomplete')
|
|
606
|
+
console.log(' Saved check history is incomplete; this run cannot confirm earlier bugs are fixed.');
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
console.log(` ${chalk.dim('Saved checks:')} unavailable for this run`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
381
612
|
if (state.error)
|
|
382
613
|
console.log(` ${chalk.dim('Error:')} ${state.error}`);
|
|
383
614
|
if (options.timedOut) {
|
|
@@ -387,23 +618,48 @@ function printHostedState(operation, state, options) {
|
|
|
387
618
|
console.log(` ${chalk.dim('Resume:')} ${nextCommand}`);
|
|
388
619
|
}
|
|
389
620
|
function markNonSuccess(state, timedOut = false) {
|
|
621
|
+
if (state.fleetResult) {
|
|
622
|
+
const result = state.fleetResult;
|
|
623
|
+
const cleanup = requireRecord(result.cleanup, 'Hosted fleet cleanup');
|
|
624
|
+
if (result.lifecycle !== 'complete' || result.runHealth !== 'healthy' || cleanup.status !== 'complete') {
|
|
625
|
+
process.exitCode = 2;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (result.savedChecks !== undefined) {
|
|
629
|
+
const saved = requireRecord(result.savedChecks, 'Hosted saved checks');
|
|
630
|
+
const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
|
|
631
|
+
if (completeness.status !== 'complete' || saved.conflicts.length > 0) {
|
|
632
|
+
process.exitCode = 2;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (saved.unresolvedFindings.length > 0)
|
|
636
|
+
process.exitCode = 1;
|
|
637
|
+
}
|
|
638
|
+
if (result.findings.some(value => requireRecord(value, 'Hosted fleet finding').judgment === 'regression')) {
|
|
639
|
+
process.exitCode = 1;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
390
642
|
if (timedOut
|
|
391
643
|
|| (state.status !== 'complete' && TERMINAL_STATUSES.has(state.status))) {
|
|
392
644
|
process.exitCode = 2;
|
|
393
645
|
}
|
|
394
646
|
}
|
|
395
647
|
export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
648
|
+
// Validate all local input before resolving accounts or submitting a job.
|
|
649
|
+
const { repository } = prepareHostedLaunch(repositoryArg, options);
|
|
650
|
+
const polling = hostedPollingOptions(options.interval, options.timeout, current => {
|
|
651
|
+
if (!options.json)
|
|
652
|
+
console.error(chalk.dim(hostedProgressMessage(current)));
|
|
653
|
+
});
|
|
400
654
|
const auth = await resolveAuthContext({
|
|
401
655
|
preferredLogin: options.account,
|
|
402
656
|
owner: repository.owner,
|
|
403
657
|
repo: repository.repository,
|
|
404
658
|
});
|
|
405
659
|
if (!options.json) {
|
|
406
|
-
console.error(chalk.dim(
|
|
660
|
+
console.error(chalk.dim(options.provider === 'haystack'
|
|
661
|
+
? 'Submitting repository revisions to the hosted fleet verifier…'
|
|
662
|
+
: 'Submitting the exact commit pair to the hosted verifier…'));
|
|
407
663
|
}
|
|
408
664
|
const started = await startHostedVerification(repositoryArg, options, auth.token);
|
|
409
665
|
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
@@ -412,11 +668,7 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
|
412
668
|
let state = started.state;
|
|
413
669
|
let timedOut;
|
|
414
670
|
if (options.wait !== false && !TERMINAL_STATUSES.has(state.status)) {
|
|
415
|
-
const waited = await waitForHostedVerification(state.runId, auth.token,
|
|
416
|
-
if (!options.json) {
|
|
417
|
-
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
418
|
-
}
|
|
419
|
-
}));
|
|
671
|
+
const waited = await waitForHostedVerification(state.runId, auth.token, polling);
|
|
420
672
|
state = waited.state;
|
|
421
673
|
timedOut = waited.timedOut;
|
|
422
674
|
}
|
|
@@ -431,7 +683,14 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
|
431
683
|
});
|
|
432
684
|
markNonSuccess(state, timedOut);
|
|
433
685
|
}
|
|
686
|
+
function hostedProgressMessage(state) {
|
|
687
|
+
return `Hosted verifier: ${state.status}${state.progress ? ` · ${state.progress.stage} (${state.progress.status})` : ''}…`;
|
|
688
|
+
}
|
|
434
689
|
export async function verifyHostedStatusCommand(runId, options) {
|
|
690
|
+
const polling = hostedPollingOptions(options.interval, options.timeout, current => {
|
|
691
|
+
if (!options.json)
|
|
692
|
+
console.error(chalk.dim(hostedProgressMessage(current)));
|
|
693
|
+
});
|
|
435
694
|
const auth = await resolveAuthContext({ preferredLogin: options.account });
|
|
436
695
|
const selectedAccount = auth.token.startsWith('hsk_live_')
|
|
437
696
|
? undefined
|
|
@@ -439,11 +698,7 @@ export async function verifyHostedStatusCommand(runId, options) {
|
|
|
439
698
|
let state = await getHostedVerification(runId, auth.token);
|
|
440
699
|
let timedOut;
|
|
441
700
|
if (options.wait && !TERMINAL_STATUSES.has(state.status)) {
|
|
442
|
-
const waited = await waitForHostedVerification(runId, auth.token,
|
|
443
|
-
if (!options.json) {
|
|
444
|
-
console.error(chalk.dim(`Hosted verifier: ${current.status}…`));
|
|
445
|
-
}
|
|
446
|
-
}));
|
|
701
|
+
const waited = await waitForHostedVerification(runId, auth.token, polling);
|
|
447
702
|
state = waited.state;
|
|
448
703
|
timedOut = waited.timedOut;
|
|
449
704
|
}
|
|
@@ -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>;
|