@haystackeditor/cli 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -24
- package/dist/commands/case-batch-contract.js +2 -2
- package/dist/commands/case-batch.js +10 -18
- package/dist/commands/crawl-contract.js +9 -0
- package/dist/commands/db-profile-contract.js +419 -0
- package/dist/commands/db-profile-database.js +289 -0
- package/dist/commands/db-profile-distinct.js +97 -0
- package/dist/commands/db-profile-owners.js +94 -0
- package/dist/commands/db-profile-privacy.js +450 -0
- package/dist/commands/db-profile-show.js +218 -0
- package/dist/commands/db-profile-sql-literals.js +140 -0
- package/dist/commands/db-profile-sql.js +362 -0
- package/dist/commands/db-profile-upload.js +88 -0
- package/dist/commands/db-profile.js +1020 -0
- package/dist/commands/fleet-policy-contract.js +28 -0
- package/dist/commands/fleet-policy.js +305 -0
- package/dist/commands/precompute-delivery-worker.js +17 -3
- package/dist/commands/precompute-delivery.js +19 -6
- package/dist/commands/verify-hosted.js +20 -0
- package/dist/commands/verify-precompute.js +161 -97
- package/dist/commands/verify.js +608 -52
- package/dist/index.js +178 -20
- package/dist/schema.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/verify-base.js +31 -0
- package/package.json +4 -1
- package/schemas/verify.v1.json +237 -0
- package/dist/commands/combination-search-hook-contract.js +0 -1
- package/dist/commands/combination-search-hook.js +0 -139
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
import { spawn, execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, constants, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { closeSync, constants, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { createGzip } from 'node:zlib';
|
|
5
5
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
|
|
9
|
-
import { findGitRoot, parseRemoteUrl,
|
|
9
|
+
import { findGitRoot, parseRemoteUrl, } from '../utils/git.js';
|
|
10
|
+
import { resolveVerifyBaseSha } from '../utils/verify-base.js';
|
|
10
11
|
import { HaystackApiError, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
|
|
11
12
|
import { trackPrecomputeEvent } from '../utils/telemetry.js';
|
|
12
13
|
import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
|
|
13
14
|
import { startPrecomputeDelivery } from './precompute-delivery.js';
|
|
14
|
-
import {
|
|
15
|
+
import { CRAWL_BUDGET_MAX_MS, CRAWL_BUDGET_MIN_MS, CRAWL_TITLE_MAX_CHARS } from './crawl-contract.js';
|
|
15
16
|
// --hook has five seconds to capture the exact source and publish local custody.
|
|
16
17
|
// Its detached sender owns the bounded network delivery and acknowledgement.
|
|
17
18
|
// An explicit invocation still waits synchronously for the server response.
|
|
18
19
|
const HOOK_WALL_MS = 5_000;
|
|
19
|
-
const EXPLICIT_WALL_MS = 60_000;
|
|
20
|
+
export const EXPLICIT_WALL_MS = 60_000;
|
|
20
21
|
const REMOTE_COMMAND_MS = 750;
|
|
21
22
|
// The published CLI cannot import verifier/contracts outside its package root.
|
|
22
23
|
const hasOnlyCharacters = (value, allowed) => value.length > 0
|
|
@@ -79,50 +80,29 @@ function resolveOriginRepository(gitRoot) {
|
|
|
79
80
|
throw new PrecomputeFailure('No supported origin repository was found.');
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
|
|
83
|
+
/** The shared base rule (utils/verify-base.ts), bounded by the hook's deadline. */
|
|
84
|
+
function deriveBaseSha(gitRoot, deadline) {
|
|
83
85
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
catch {
|
|
90
|
-
// A clone does not have to carry origin/HEAD; ask the remote below.
|
|
86
|
+
return resolveVerifyBaseSha('HEAD', {
|
|
87
|
+
run: (args, options) => gitOutput(args, gitRoot, deadline, options?.remote
|
|
88
|
+
? { env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, timeoutCapMs: REMOTE_COMMAND_MS } : {}),
|
|
89
|
+
fetchOptions: () => ({ cwd: gitRoot, timeoutMs: Math.max(1, Math.min(remainingMs(deadline), REMOTE_COMMAND_MS)) }),
|
|
90
|
+
});
|
|
91
91
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const prefix = 'ref: refs/heads/';
|
|
97
|
-
const heads = remoteRefs.split('\n').map(line => line.split('\t'))
|
|
98
|
-
.filter(fields => fields.length === 2 && fields[1] === 'HEAD' && fields[0].startsWith(prefix))
|
|
99
|
-
.map(fields => fields[0].slice(prefix.length));
|
|
100
|
-
if (heads.length !== 1 || heads[0].length === 0) {
|
|
101
|
-
throw new PrecomputeFailure('Could not resolve the origin default branch.');
|
|
102
|
-
}
|
|
103
|
-
gitOutput(['check-ref-format', `refs/heads/${heads[0]}`], gitRoot, deadline);
|
|
104
|
-
return heads[0];
|
|
105
|
-
}
|
|
106
|
-
function deriveBaseSha(gitRoot, deadline) {
|
|
107
|
-
const defaultBranch = resolveOriginDefaultBranch(gitRoot, deadline);
|
|
108
|
-
const baseRef = resolveDiffBaseRef(defaultBranch, {
|
|
109
|
-
cwd: gitRoot,
|
|
110
|
-
timeoutMs: Math.max(1, Math.min(remainingMs(deadline), REMOTE_COMMAND_MS)),
|
|
111
|
-
});
|
|
112
|
-
const baseSha = gitOutput(['merge-base', 'HEAD', baseRef], gitRoot, deadline);
|
|
113
|
-
if (!isCommitSha(baseSha)) {
|
|
114
|
-
throw new PrecomputeFailure('Could not derive the base commit.');
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof PrecomputeFailure)
|
|
94
|
+
throw error;
|
|
95
|
+
throw new PrecomputeFailure(error instanceof Error ? error.message : 'Could not derive the base commit.');
|
|
115
96
|
}
|
|
116
|
-
return baseSha;
|
|
117
97
|
}
|
|
118
98
|
// The -U0 scan text omits binary content and context, so it stays far below
|
|
119
99
|
// the uploaded patch; this only guards against git's 1 MiB default.
|
|
120
100
|
const SCAN_PATCH_MAX_BYTES = 256 * 1024 * 1024;
|
|
121
101
|
/**
|
|
122
102
|
* Withholds secret-bearing changes from the snapshot index before the tree is
|
|
123
|
-
* written, so the tree, work commit
|
|
124
|
-
*
|
|
125
|
-
*
|
|
103
|
+
* written, so the tree, work commit and uploaded patch all agree. A withheld
|
|
104
|
+
* path keeps its base version (or is absent when base has none). Every
|
|
105
|
+
* withheld path is named on stderr; secret values never are.
|
|
126
106
|
*/
|
|
127
107
|
function withholdSecretBearingChanges(gitRoot, baseSha, indexEnv, deadline) {
|
|
128
108
|
const changed = gitOutput(['diff', '--cached', '--no-renames', '--diff-filter=d', '--name-only', '-z', baseSha, '--'], gitRoot, deadline, { env: indexEnv, maxBuffer: SCAN_PATCH_MAX_BYTES, raw: true }).split('\0').filter(path => path.length > 0);
|
|
@@ -186,17 +166,40 @@ function deriveWorkCommit(gitRoot, baseSha, treeSha, deadline) {
|
|
|
186
166
|
}
|
|
187
167
|
// A clean checkout of the base already has the exact required identity.
|
|
188
168
|
if (baseSha === headSha && treeSha === headTreeSha)
|
|
189
|
-
return headSha;
|
|
169
|
+
return { workCommit: headSha, headSha, headTreeSha };
|
|
190
170
|
const workCommit = gitOutput(['commit-tree', treeSha, '-p', baseSha, '-m', 'front-half-snapshot'], gitRoot, deadline, { env: { ...process.env, ...FIXED_COMMIT_ENV } });
|
|
191
171
|
if (!isCommitSha(workCommit)) {
|
|
192
172
|
throw new PrecomputeFailure('Could not derive the working commit.');
|
|
193
173
|
}
|
|
194
|
-
return workCommit;
|
|
174
|
+
return { workCommit, headSha, headTreeSha };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* CrawlSubmission.changeTitle (CRAWL-V1): HEAD's subject when HEAD differs
|
|
178
|
+
* from base, then " (with uncommitted changes)" when the captured tree differs
|
|
179
|
+
* from HEAD's; "Uncommitted changes" when HEAD is base. A subject too long for
|
|
180
|
+
* 200 characters is cut at a character boundary, never the suffix.
|
|
181
|
+
*/
|
|
182
|
+
function deriveChangeTitle(gitRoot, baseSha, treeSha, commits, deadline) {
|
|
183
|
+
if (commits.headSha === baseSha)
|
|
184
|
+
return 'Uncommitted changes';
|
|
185
|
+
// A commit with an empty message is named by its id, so the title is never empty.
|
|
186
|
+
const subject = gitOutput(['log', '-1', '--no-show-signature', '--encoding=UTF-8', '--format=%s', commits.headSha, '--'], gitRoot, deadline, { raw: true }).replace(/\n$/, '') || `Commit ${commits.headSha.slice(0, 12)}`;
|
|
187
|
+
const suffix = treeSha === commits.headTreeSha ? '' : ' (with uncommitted changes)';
|
|
188
|
+
let title = '';
|
|
189
|
+
for (const character of subject) {
|
|
190
|
+
if (title.length + character.length + suffix.length > CRAWL_TITLE_MAX_CHARS)
|
|
191
|
+
break;
|
|
192
|
+
title += character;
|
|
193
|
+
}
|
|
194
|
+
return title + suffix;
|
|
195
195
|
}
|
|
196
196
|
function slimInputHash() {
|
|
197
|
-
// Mirrors
|
|
197
|
+
// Mirrors front_half_cache_derive in blast-cache.sh for a slim bundle:
|
|
198
|
+
// printf '%s\0%s\0%s\0%s\0%s\0' slim agent-deferred none none crawl-blast-v1.
|
|
199
|
+
// The fifth field (CRAWL-V1 rule 4) keeps bundles written before
|
|
200
|
+
// crawl-blast.json existed from being restored for a crawl.
|
|
198
201
|
return createHash('sha256')
|
|
199
|
-
.update(Buffer.from('slim\0agent-deferred\0none\0none\0'))
|
|
202
|
+
.update(Buffer.from('slim\0agent-deferred\0none\0none\0crawl-blast-v1\0'))
|
|
200
203
|
.digest('hex');
|
|
201
204
|
}
|
|
202
205
|
async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
|
@@ -256,13 +259,17 @@ async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
|
|
256
259
|
child.stdout.pipe(gzip);
|
|
257
260
|
});
|
|
258
261
|
}
|
|
259
|
-
export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + HOOK_WALL_MS
|
|
262
|
+
export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + HOOK_WALL_MS,
|
|
263
|
+
/** How long the crawl may take (CRAWL-V1 amendment 8); undefined: the service's default. */
|
|
264
|
+
budgetMs) {
|
|
260
265
|
// Capture this before git and compression work so delivery latency cannot
|
|
261
266
|
// make an older stop look newer than a later one.
|
|
262
267
|
const observedAt = Date.now();
|
|
263
268
|
const baseSha = deriveBaseSha(gitRoot, deadline);
|
|
264
269
|
const treeSha = deriveWorktreeTreeSha(gitRoot, baseSha, deadline);
|
|
265
|
-
const
|
|
270
|
+
const commits = deriveWorkCommit(gitRoot, baseSha, treeSha, deadline);
|
|
271
|
+
const { workCommit } = commits;
|
|
272
|
+
const changeTitle = deriveChangeTitle(gitRoot, baseSha, treeSha, commits, deadline);
|
|
266
273
|
const patchGzip = await buildCompressedPatch(gitRoot, baseSha, workCommit, deadline);
|
|
267
274
|
const cacheKey = [
|
|
268
275
|
baseSha.slice(0, 12),
|
|
@@ -271,7 +278,6 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
|
|
|
271
278
|
'slim',
|
|
272
279
|
slimInputHash(),
|
|
273
280
|
].join('-');
|
|
274
|
-
const combinationSearch = captureCombinationSearchHook(gitRoot, `${owner}/${repository}`, baseSha, workCommit, deadline);
|
|
275
281
|
return {
|
|
276
282
|
treeSha,
|
|
277
283
|
request: {
|
|
@@ -283,7 +289,7 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
|
|
|
283
289
|
cacheKey,
|
|
284
290
|
patchGzBase64: patchGzip.toString('base64'),
|
|
285
291
|
patchSha256: createHash('sha256').update(patchGzip).digest('hex'),
|
|
286
|
-
...(
|
|
292
|
+
...(workCommit === baseSha ? {} : { crawl: { treeSha, changeTitle, ...(budgetMs === undefined ? {} : { budgetMs }) } }),
|
|
287
293
|
},
|
|
288
294
|
};
|
|
289
295
|
}
|
|
@@ -291,16 +297,17 @@ function precomputeResult(response) {
|
|
|
291
297
|
if (!response || typeof response !== 'object' || Array.isArray(response)) {
|
|
292
298
|
throw new PrecomputeFailure('The precompute service returned an invalid response.');
|
|
293
299
|
}
|
|
294
|
-
const { status, runId, walker: rawWalker,
|
|
300
|
+
const { status, runId, walker: rawWalker, crawl: rawCrawl } = response;
|
|
295
301
|
const walker = rawWalker === undefined ? undefined : readWalkerStatus(rawWalker);
|
|
296
|
-
const
|
|
302
|
+
const crawl = rawCrawl === undefined ? undefined : readCrawlHookStatus(rawCrawl);
|
|
303
|
+
const reported = { ...(walker === undefined ? {} : { walker }), ...(crawl === undefined ? {} : { crawl }) };
|
|
297
304
|
if (status === 'queued' && typeof runId === 'string' && isRunId(runId)) {
|
|
298
|
-
return { outcome: 'enqueued', runId, ...
|
|
305
|
+
return { outcome: 'enqueued', runId, ...reported };
|
|
299
306
|
}
|
|
300
307
|
if (status === 'cached')
|
|
301
|
-
return { outcome: 'cached', ...
|
|
308
|
+
return { outcome: 'cached', ...reported };
|
|
302
309
|
if (status === 'existing')
|
|
303
|
-
return { outcome: 'existing', ...
|
|
310
|
+
return { outcome: 'existing', ...reported };
|
|
304
311
|
throw new PrecomputeFailure('The precompute service returned an invalid response.');
|
|
305
312
|
}
|
|
306
313
|
/** One authenticated submission of the already captured request. No source reads or retries. */
|
|
@@ -325,6 +332,31 @@ function readWalkerStatus(value) {
|
|
|
325
332
|
}
|
|
326
333
|
throw new PrecomputeFailure('The precompute service returned an invalid walker status.');
|
|
327
334
|
}
|
|
335
|
+
const CRAWL_UNAVAILABLE_REASONS = ['not-configured', 'runner-unavailable', 'profile-ambiguous', 'superseded', 'enqueue-failed'];
|
|
336
|
+
function readCrawlHookStatus(value) {
|
|
337
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
338
|
+
const record = value;
|
|
339
|
+
if (Object.keys(record).length === 2) {
|
|
340
|
+
if ((record.status === 'queued' || record.status === 'existing')
|
|
341
|
+
&& typeof record.runId === 'string' && isRunId(record.runId))
|
|
342
|
+
return { status: record.status, runId: record.runId };
|
|
343
|
+
const reason = CRAWL_UNAVAILABLE_REASONS.find(known => known === record.reason);
|
|
344
|
+
if (record.status === 'unavailable' && reason !== undefined)
|
|
345
|
+
return { status: record.status, reason };
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
throw new PrecomputeFailure('The precompute service returned an invalid crawl status.');
|
|
349
|
+
}
|
|
350
|
+
/** Why the service could not start a crawl, in plain words. */
|
|
351
|
+
export function crawlUnavailableText(reason) {
|
|
352
|
+
switch (reason) {
|
|
353
|
+
case 'not-configured': return 'this repository is not set up for crawling yet';
|
|
354
|
+
case 'runner-unavailable': return 'no machine that can crawl this repository is available right now';
|
|
355
|
+
case 'profile-ambiguous': return 'the machines that crawl this repository disagree about how to run it';
|
|
356
|
+
case 'superseded': return 'a newer stop in this repository came first';
|
|
357
|
+
case 'enqueue-failed': return 'the service could not queue it';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
328
360
|
function walkerMetadataDirectory(gitRoot, deadline) {
|
|
329
361
|
const common = gitOutput(['rev-parse', '--git-common-dir'], gitRoot, deadline);
|
|
330
362
|
return join(isAbsolute(common) ? common : resolve(gitRoot, common), 'haystack');
|
|
@@ -361,7 +393,8 @@ function saveWalkerStatus(directory, request, walker) {
|
|
|
361
393
|
rmSync(temporary, { force: true });
|
|
362
394
|
}
|
|
363
395
|
}
|
|
364
|
-
|
|
396
|
+
/** The one-line error an explicit (non-hook) submission reports. */
|
|
397
|
+
export function normalModeFailure(error) {
|
|
365
398
|
if (error instanceof PrecomputeFailure)
|
|
366
399
|
return error;
|
|
367
400
|
if (error instanceof NotLoggedInError) {
|
|
@@ -389,7 +422,8 @@ function normalModeFailure(error) {
|
|
|
389
422
|
}
|
|
390
423
|
return new PrecomputeFailure('The precompute request failed.');
|
|
391
424
|
}
|
|
392
|
-
function printResult(
|
|
425
|
+
function printResult(result) {
|
|
426
|
+
const { outcome, runId, walker, crawl } = result;
|
|
393
427
|
if (outcome === 'enqueued') {
|
|
394
428
|
console.log(chalk.green(`Precompute request queued${runId ? ` (${runId})` : ''}.`));
|
|
395
429
|
}
|
|
@@ -409,10 +443,72 @@ function printResult(outcome, runId, walker, search) {
|
|
|
409
443
|
: 'Walker unavailable: the enqueue request failed.');
|
|
410
444
|
else
|
|
411
445
|
console.log('Walker status was not reported by the service.');
|
|
412
|
-
if (
|
|
413
|
-
console.log(`
|
|
414
|
-
else if (
|
|
415
|
-
console.log(`
|
|
446
|
+
if (crawl?.status === 'queued')
|
|
447
|
+
console.log(`Crawl queued (${crawl.runId}).`);
|
|
448
|
+
else if (crawl?.status === 'existing')
|
|
449
|
+
console.log(`A matching crawl already exists (${crawl.runId}).`);
|
|
450
|
+
else if (crawl?.status === 'unavailable')
|
|
451
|
+
console.log(`Crawl unavailable: ${crawlUnavailableText(crawl.reason)}.`);
|
|
452
|
+
else
|
|
453
|
+
console.log('Crawl status was not reported by the service.');
|
|
454
|
+
}
|
|
455
|
+
/** The hook, an explicit precompute and `haystack verify` all capture here.
|
|
456
|
+
* The repository defaults to origin's, as the hook's does. */
|
|
457
|
+
/** The repository's crawl time: .haystack.json crawl.minutes at the git root, whole minutes from 1 to 30 (CRAWL-V1 amendment 8),
|
|
458
|
+
* or undefined when the file or the field is absent. A value out of range fails the capture, never silently becomes the default. */
|
|
459
|
+
function configuredCrawlMinutes(gitRoot) {
|
|
460
|
+
const path = join(gitRoot, '.haystack.json');
|
|
461
|
+
if (!existsSync(path))
|
|
462
|
+
return undefined;
|
|
463
|
+
const minutes = JSON.parse(readFileSync(path, 'utf8')).crawl?.minutes;
|
|
464
|
+
if (minutes === undefined)
|
|
465
|
+
return undefined;
|
|
466
|
+
if (typeof minutes !== 'number' || !Number.isInteger(minutes) || minutes * 60_000 < CRAWL_BUDGET_MIN_MS || minutes * 60_000 > CRAWL_BUDGET_MAX_MS) {
|
|
467
|
+
throw new PrecomputeFailure(`.haystack.json crawl.minutes must be a whole number from ${CRAWL_BUDGET_MIN_MS / 60_000} to ${CRAWL_BUDGET_MAX_MS / 60_000}.`);
|
|
468
|
+
}
|
|
469
|
+
return minutes;
|
|
470
|
+
}
|
|
471
|
+
export async function captureCheckout(deadline, repository,
|
|
472
|
+
/** `haystack verify --minutes`; undefined: the repository's .haystack.json crawl.minutes, else the service's default. */
|
|
473
|
+
minutes) {
|
|
474
|
+
const gitRoot = findGitRoot();
|
|
475
|
+
if (!gitRoot) {
|
|
476
|
+
throw new PrecomputeFailure('Run this command from a git repository.');
|
|
477
|
+
}
|
|
478
|
+
const metadataDirectory = walkerMetadataDirectory(gitRoot, deadline);
|
|
479
|
+
const target = repository ?? resolveOriginRepository(gitRoot);
|
|
480
|
+
const chosen = minutes ?? configuredCrawlMinutes(gitRoot);
|
|
481
|
+
const derivation = await derivePrecomputeRequest(gitRoot, target.owner, target.repository, deadline, chosen === undefined ? undefined : chosen * 60_000);
|
|
482
|
+
return { metadataDirectory, derivation };
|
|
483
|
+
}
|
|
484
|
+
/** The explicit (non-hook) submission of a capture: one synchronous
|
|
485
|
+
* authenticated POST, bounded by the deadline, and its walker status saved.
|
|
486
|
+
* `haystack verify precompute` and `haystack verify` both submit here. */
|
|
487
|
+
export async function postPrecomputeCapture(capture, token, deadline) {
|
|
488
|
+
const controller = new AbortController();
|
|
489
|
+
const fetchTimer = setTimeout(() => controller.abort(), remainingMs(deadline));
|
|
490
|
+
let response;
|
|
491
|
+
try {
|
|
492
|
+
response = await haystackJson('/api/agent/cloud-verifier/front-half', token, {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers: { 'Content-Type': 'application/json' },
|
|
495
|
+
body: JSON.stringify(capture.derivation.request),
|
|
496
|
+
signal: controller.signal,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
finally {
|
|
500
|
+
clearTimeout(fetchTimer);
|
|
501
|
+
}
|
|
502
|
+
const result = precomputeResult(response);
|
|
503
|
+
if (result.walker) {
|
|
504
|
+
try {
|
|
505
|
+
saveWalkerStatus(capture.metadataDirectory, capture.derivation.request, result.walker);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
throw new PrecomputeFailure('Precompute response received, but its walker status could not be saved.');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return result;
|
|
416
512
|
}
|
|
417
513
|
export async function verifyPrecomputeCommand(options) {
|
|
418
514
|
const startedAt = Date.now();
|
|
@@ -420,61 +516,29 @@ export async function verifyPrecomputeCommand(options) {
|
|
|
420
516
|
const deadline = startedAt + totalWallMs;
|
|
421
517
|
let outcome = 'error';
|
|
422
518
|
try {
|
|
423
|
-
const
|
|
424
|
-
if (!gitRoot) {
|
|
425
|
-
throw new PrecomputeFailure('Run this command from a git repository.');
|
|
426
|
-
}
|
|
427
|
-
const metadataDirectory = walkerMetadataDirectory(gitRoot, deadline);
|
|
428
|
-
const repository = resolveOriginRepository(gitRoot);
|
|
429
|
-
const derivation = await derivePrecomputeRequest(gitRoot, repository.owner, repository.repository, deadline);
|
|
519
|
+
const capture = await captureCheckout(deadline);
|
|
430
520
|
if (options.hook) {
|
|
431
521
|
remainingMs(deadline);
|
|
432
|
-
await startPrecomputeDelivery(metadataDirectory, derivation.request);
|
|
522
|
+
await startPrecomputeDelivery(capture.metadataDirectory, capture.derivation.request);
|
|
433
523
|
outcome = 'pending';
|
|
434
524
|
return;
|
|
435
525
|
}
|
|
436
526
|
let token;
|
|
437
527
|
try {
|
|
438
528
|
const auth = await resolveAuthContext({
|
|
439
|
-
owner:
|
|
440
|
-
repo:
|
|
529
|
+
owner: capture.derivation.request.owner,
|
|
530
|
+
repo: capture.derivation.request.repository,
|
|
441
531
|
});
|
|
442
532
|
token = auth.token;
|
|
443
533
|
}
|
|
444
534
|
catch (error) {
|
|
445
|
-
if (error instanceof NotLoggedInError)
|
|
535
|
+
if (error instanceof NotLoggedInError)
|
|
446
536
|
outcome = 'skipped_no_auth';
|
|
447
|
-
if (options.hook)
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
537
|
throw error;
|
|
451
538
|
}
|
|
452
|
-
const
|
|
453
|
-
const fetchTimer = setTimeout(() => controller.abort(), remainingMs(deadline));
|
|
454
|
-
let response;
|
|
455
|
-
try {
|
|
456
|
-
response = await haystackJson('/api/agent/cloud-verifier/front-half', token, {
|
|
457
|
-
method: 'POST',
|
|
458
|
-
headers: { 'Content-Type': 'application/json' },
|
|
459
|
-
body: JSON.stringify(derivation.request),
|
|
460
|
-
signal: controller.signal,
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
finally {
|
|
464
|
-
clearTimeout(fetchTimer);
|
|
465
|
-
}
|
|
466
|
-
const result = precomputeResult(response);
|
|
539
|
+
const result = await postPrecomputeCapture(capture, token, deadline);
|
|
467
540
|
outcome = result.outcome;
|
|
468
|
-
|
|
469
|
-
try {
|
|
470
|
-
saveWalkerStatus(metadataDirectory, derivation.request, result.walker);
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
throw new PrecomputeFailure('Precompute response received, but its walker status could not be saved.');
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
if (!options.hook)
|
|
477
|
-
printResult(outcome, result.runId, result.walker, result.search);
|
|
541
|
+
printResult(result);
|
|
478
542
|
}
|
|
479
543
|
catch (error) {
|
|
480
544
|
if (!options.hook)
|