@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.
Files changed (32) hide show
  1. package/dist/assets/telemetry/browser-runtime.js +148 -2
  2. package/dist/commands/design-verify.d.ts +2 -0
  3. package/dist/commands/design-verify.js +27 -1
  4. package/dist/commands/precompute-delivery-contract.d.ts +68 -0
  5. package/dist/commands/precompute-delivery-contract.js +1 -0
  6. package/dist/commands/precompute-delivery-worker.d.ts +1 -0
  7. package/dist/commands/precompute-delivery-worker.js +68 -0
  8. package/dist/commands/precompute-delivery.d.ts +20 -0
  9. package/dist/commands/precompute-delivery.js +319 -0
  10. package/dist/commands/verify-explore.d.ts +14 -0
  11. package/dist/commands/verify-explore.js +180 -0
  12. package/dist/commands/verify-history.d.ts +14 -0
  13. package/dist/commands/verify-history.js +152 -0
  14. package/dist/commands/verify-hosted.d.ts +12 -2
  15. package/dist/commands/verify-hosted.js +305 -50
  16. package/dist/commands/verify-precompute.d.ts +12 -1
  17. package/dist/commands/verify-precompute.js +135 -27
  18. package/dist/commands/verify.d.ts +6 -0
  19. package/dist/commands/verify.js +22 -0
  20. package/dist/index.js +71 -5
  21. package/dist/types/verify-history.d.ts +38 -0
  22. package/dist/types/verify-history.js +1 -0
  23. package/dist/utils/design-verifier-api.d.ts +8 -0
  24. package/dist/utils/design-verifier-api.js +23 -0
  25. package/dist/utils/design-verifier-history.d.ts +46 -0
  26. package/dist/utils/design-verifier-history.js +79 -0
  27. package/dist/utils/design-verifier-result.d.ts +17 -0
  28. package/dist/utils/design-verifier-result.js +80 -1
  29. package/dist/utils/git.js +5 -1
  30. package/dist/utils/telemetry.js +8 -2
  31. package/package.json +1 -1
  32. 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 result = resultValue === undefined
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
- if (status === 'complete') {
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(replayPlanFrom
219
- ? [repository, baseCommit, headCommit, replayPlanFrom]
220
- : [repository, baseCommit, headCommit]))
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 baseCommit = requireCommit(options.base, '--base');
249
- const headCommit = requireCommit(options.head, '--head');
250
- const intent = readHostedIntentFile(options.intentFile);
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
- const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
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 lastStatus;
495
+ let lastProgress;
307
496
  for (;;) {
308
497
  const state = await getHostedVerification(runId, token);
309
- if (state.status !== lastStatus) {
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
- lastStatus = state.status;
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.green('✓')
362
- : TERMINAL_STATUSES.has(state.status)
363
- ? chalk.red('✗')
364
- : chalk.cyan('•');
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
- const repository = parseHostedRepository(repositoryArg);
397
- // Fail before account resolution so an invalid local intent never advances
398
- // toward a hosted launch.
399
- readHostedIntentFile(options.intentFile);
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('Submitting the exact commit pair to the hosted verifier…'));
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, hostedPollingOptions(options.interval, options.timeout, current => {
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, hostedPollingOptions(options.interval, options.timeout, current => {
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
- export type PrecomputeOutcome = 'enqueued' | 'cached' | 'existing' | 'skipped_no_auth' | 'error';
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>;