@haystackeditor/cli 0.20.0 → 0.21.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/dist/commands/verify-precompute.js +16 -10
- package/dist/commands/verify.js +22 -12
- package/dist/index.js +8 -7
- package/package.json +1 -1
|
@@ -262,7 +262,9 @@ async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
|
|
262
262
|
}
|
|
263
263
|
export async function derivePrecomputeRequest(gitRoot, owner, repository, deadline = Date.now() + HOOK_WALL_MS,
|
|
264
264
|
/** How long the crawl may take (CRAWL-V1 amendment 8); undefined: the service's default. */
|
|
265
|
-
budgetMs
|
|
265
|
+
budgetMs,
|
|
266
|
+
/** Amendment 12: 'prepare' builds and freezes only (the turn-end hook); 'crawl' is `haystack verify`. */
|
|
267
|
+
mode = 'crawl') {
|
|
266
268
|
// Capture this before git and compression work so delivery latency cannot
|
|
267
269
|
// make an older stop look newer than a later one.
|
|
268
270
|
const observedAt = Date.now();
|
|
@@ -290,7 +292,7 @@ budgetMs) {
|
|
|
290
292
|
cacheKey,
|
|
291
293
|
patchGzBase64: patchGzip.toString('base64'),
|
|
292
294
|
patchSha256: createHash('sha256').update(patchGzip).digest('hex'),
|
|
293
|
-
...(workCommit === baseSha ? {} : { crawl: { treeSha, changeTitle, ...(budgetMs === undefined ? {} : { budgetMs }) } }),
|
|
295
|
+
...(workCommit === baseSha ? {} : { crawl: { treeSha, changeTitle, ...(budgetMs === undefined ? {} : { budgetMs }), mode } }),
|
|
294
296
|
},
|
|
295
297
|
};
|
|
296
298
|
}
|
|
@@ -333,7 +335,7 @@ function readWalkerStatus(value) {
|
|
|
333
335
|
}
|
|
334
336
|
throw new PrecomputeFailure('The precompute service returned an invalid walker status.');
|
|
335
337
|
}
|
|
336
|
-
const CRAWL_UNAVAILABLE_REASONS = ['runner-unavailable', '
|
|
338
|
+
const CRAWL_UNAVAILABLE_REASONS = ['runner-unavailable', 'enqueue-failed'];
|
|
337
339
|
const isOnboardRunId = (value) => typeof value === 'string' && /^ob_[0-9a-f]{48}$/.test(value);
|
|
338
340
|
function readCrawlHookStatus(value) {
|
|
339
341
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
@@ -365,7 +367,6 @@ function readCrawlHookStatus(value) {
|
|
|
365
367
|
export function crawlUnavailableText(reason) {
|
|
366
368
|
switch (reason) {
|
|
367
369
|
case 'runner-unavailable': return 'no machine that can crawl this repository is available right now';
|
|
368
|
-
case 'superseded': return 'a newer stop in this repository came first';
|
|
369
370
|
case 'enqueue-failed': return 'the service could not queue it';
|
|
370
371
|
}
|
|
371
372
|
}
|
|
@@ -455,14 +456,16 @@ function printResult(result) {
|
|
|
455
456
|
: 'Walker unavailable: the enqueue request failed.');
|
|
456
457
|
else
|
|
457
458
|
console.log('Walker status was not reported by the service.');
|
|
459
|
+
// Amendment 12: this capture builds and freezes the app only; `haystack verify` crawls it.
|
|
458
460
|
if (crawl?.status === 'queued')
|
|
459
|
-
console.log(`
|
|
461
|
+
console.log(`Building and freezing the app for this change (${crawl.runId}); run \`haystack verify\` to check it.`);
|
|
460
462
|
else if (crawl?.status === 'existing')
|
|
461
|
-
console.log(`
|
|
463
|
+
console.log(`This change is already built or being built (${crawl.runId}); run \`haystack verify\` to check it.`);
|
|
462
464
|
else if (crawl?.status === 'unavailable')
|
|
463
465
|
console.log(`Crawl unavailable: ${crawlUnavailableText(crawl.reason)}.`);
|
|
464
466
|
else if (crawl?.status === 'onboarding') {
|
|
465
|
-
console.log(`The app is being onboarded for this base (${crawl.onboardRunId});
|
|
467
|
+
console.log(`The app is being onboarded for this base (${crawl.onboardRunId}); this change is built and frozen when it is ready.`
|
|
468
|
+
+ ' Run `haystack verify` to check it.');
|
|
466
469
|
}
|
|
467
470
|
else if (crawl?.status === 'onboarding-blocked')
|
|
468
471
|
console.log(blockLines(crawl.block).join('\n'));
|
|
@@ -485,7 +488,9 @@ function configuredCrawlMinutes(gitRoot) {
|
|
|
485
488
|
}
|
|
486
489
|
return minutes;
|
|
487
490
|
}
|
|
488
|
-
export async function captureCheckout(deadline,
|
|
491
|
+
export async function captureCheckout(deadline,
|
|
492
|
+
/** Amendment 12: the turn-end hook and `verify precompute` ask to prepare (build and freeze); `haystack verify` to crawl. */
|
|
493
|
+
mode, repository,
|
|
489
494
|
/** `haystack verify --minutes`; undefined: the repository's .haystack.json crawl.minutes, else the service's default. */
|
|
490
495
|
minutes) {
|
|
491
496
|
const gitRoot = findGitRoot();
|
|
@@ -495,7 +500,7 @@ minutes) {
|
|
|
495
500
|
const metadataDirectory = walkerMetadataDirectory(gitRoot, deadline);
|
|
496
501
|
const target = repository ?? resolveOriginRepository(gitRoot);
|
|
497
502
|
const chosen = minutes ?? configuredCrawlMinutes(gitRoot);
|
|
498
|
-
const derivation = await derivePrecomputeRequest(gitRoot, target.owner, target.repository, deadline, chosen === undefined ? undefined : chosen * 60_000);
|
|
503
|
+
const derivation = await derivePrecomputeRequest(gitRoot, target.owner, target.repository, deadline, chosen === undefined ? undefined : chosen * 60_000, mode);
|
|
499
504
|
return { metadataDirectory, derivation };
|
|
500
505
|
}
|
|
501
506
|
/** The explicit (non-hook) submission of a capture: one synchronous
|
|
@@ -533,7 +538,8 @@ export async function verifyPrecomputeCommand(options) {
|
|
|
533
538
|
const deadline = startedAt + totalWallMs;
|
|
534
539
|
let outcome = 'error';
|
|
535
540
|
try {
|
|
536
|
-
|
|
541
|
+
// The hook and an explicit precompute build and freeze the change ahead of `haystack verify`; they never crawl it.
|
|
542
|
+
const capture = await captureCheckout(deadline, 'prepare');
|
|
537
543
|
if (options.hook) {
|
|
538
544
|
remainingMs(deadline);
|
|
539
545
|
await startPrecomputeDelivery(capture.metadataDirectory, capture.derivation.request);
|
package/dist/commands/verify.js
CHANGED
|
@@ -36,8 +36,8 @@ const CRAWLS_PATH = '/api/agent/cloud-verifier/crawls';
|
|
|
36
36
|
const HELD_READ_TIMEOUT_MS = CRAWL_WAIT_MAX_MS + GATEWAY_TIMEOUT_MS;
|
|
37
37
|
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
38
38
|
const REPOSITORY_COMPONENT = /^[A-Za-z0-9_.-]{1,100}$/;
|
|
39
|
-
const STATUSES = ['queued', 'preparing', 'crawling', 'draining', 'completed', 'incomplete', 'cancelled'];
|
|
40
|
-
const TERMINAL = new Set(['completed', 'incomplete', 'cancelled']);
|
|
39
|
+
const STATUSES = ['queued', 'preparing', 'crawling', 'draining', 'completed', 'prepared', 'incomplete', 'cancelled'];
|
|
40
|
+
const TERMINAL = new Set(['completed', 'prepared', 'incomplete', 'cancelled']);
|
|
41
41
|
const STAGES = ['reconcile', 'source', 'worlds', 'blast', 'knobs', 'prepare', 'crawl', 'publish', 'cleanup'];
|
|
42
42
|
const ERROR_CODES = ['source-mismatch', 'blast-unavailable', 'world-build-failed',
|
|
43
43
|
'prepare-failed', 'crawl-failed', 'no-capacity', 'stalled', 'lease-lost', 'cancelled'];
|
|
@@ -50,6 +50,7 @@ const STATUS_WORDS = {
|
|
|
50
50
|
crawling: 'exploring the app',
|
|
51
51
|
draining: 'finishing the last checks',
|
|
52
52
|
completed: 'finished',
|
|
53
|
+
prepared: 'built and ready to check',
|
|
53
54
|
incomplete: 'stopped before finishing',
|
|
54
55
|
cancelled: 'cancelled',
|
|
55
56
|
};
|
|
@@ -213,7 +214,7 @@ export function parseCrawlView(value, expected) {
|
|
|
213
214
|
|| typeof value.changeTitle !== 'string' || !isCount(value.observedAt)
|
|
214
215
|
|| !isTimestamp(value.createdAt) || !isTimestamp(value.updatedAt)
|
|
215
216
|
|| (value.supersededBy !== null && (typeof value.supersededBy !== 'string' || !RUN_ID.test(value.supersededBy)))
|
|
216
|
-
|| typeof value.cancelRequested !== 'boolean')
|
|
217
|
+
|| typeof value.cancelRequested !== 'boolean' || (value.mode !== 'prepare' && value.mode !== 'crawl'))
|
|
217
218
|
invalid('its status');
|
|
218
219
|
const clocks = value.clocks;
|
|
219
220
|
if (!isRecord(clocks) || !['startedAt', 'stageStartedAt', 'crawlStartedAt', 'finishedAt']
|
|
@@ -333,13 +334,15 @@ function headline(view) {
|
|
|
333
334
|
const error = results(view)?.error ?? view.error;
|
|
334
335
|
return `The crawl could not finish: ${error ? errorWords(error) : 'no reason was recorded'}.${found}`;
|
|
335
336
|
}
|
|
337
|
+
case 'prepared':
|
|
338
|
+
// Amendment 12: the turn-end hook built and froze this change; `haystack verify` asks for the crawl itself.
|
|
339
|
+
return 'Your change is built and ready to check, but was not crawled yet. Run `haystack verify` to check it.';
|
|
336
340
|
case 'cancelled':
|
|
337
|
-
return `The crawl was cancelled
|
|
341
|
+
return `The crawl was cancelled.${found}`;
|
|
338
342
|
default: {
|
|
339
343
|
const so = bugs > 0 ? ` ${plural(bugs, 'bug')} found so far.` : '';
|
|
340
344
|
if (view.cancelRequested) {
|
|
341
|
-
return `Being cancelled${view
|
|
342
|
-
+ `${currentStep(view)}.${so}`;
|
|
345
|
+
return `Being cancelled: ${currentStep(view)}.${so}`;
|
|
343
346
|
}
|
|
344
347
|
return `Still running: ${currentStep(view)}.${so}`;
|
|
345
348
|
}
|
|
@@ -499,9 +502,9 @@ async function waitForCrawl(first, identity, token) {
|
|
|
499
502
|
// The followed crawl's own read is held, so only a change to it wakes the wait (Astra review: a capture read could sit up to
|
|
500
503
|
// 25 s on another title's crawl of the same code).
|
|
501
504
|
let next = await readCrawlRun(view.runId, identity, token, view.updatedAt);
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
if (next.
|
|
505
|
+
// Once this crawl is being cancelled, the capture's newest crawl is read at once (not held) and followed if it is this same
|
|
506
|
+
// change (amendment 12: no crawl replaces another, but one can be cancelled and asked for again).
|
|
507
|
+
if (next.cancelRequested) {
|
|
505
508
|
const newest = await readCrawl(identity, token);
|
|
506
509
|
if (newest !== null && sameChange(newest, identity) && newest.runId !== view.runId)
|
|
507
510
|
next = newest;
|
|
@@ -530,7 +533,10 @@ function staleCrawl(found, identity) {
|
|
|
530
533
|
return 'other-time';
|
|
531
534
|
if (found.status === 'cancelled' || found.status === 'incomplete')
|
|
532
535
|
return found.status;
|
|
533
|
-
|
|
536
|
+
if (!TERMINAL.has(found.status) && found.cancelRequested)
|
|
537
|
+
return 'cancelling';
|
|
538
|
+
// A `prepared` run is asked for its crawl whatever its mode (a cancel can leave a widened one prepared).
|
|
539
|
+
return found.mode === 'prepare' || found.status === 'prepared' ? 'prepare-only' : null;
|
|
534
540
|
}
|
|
535
541
|
function staleWords(found, stale) {
|
|
536
542
|
switch (stale) {
|
|
@@ -539,6 +545,9 @@ function staleWords(found, stale) {
|
|
|
539
545
|
case 'cancelled': return `This change's last crawl was cancelled (${found.runId})`;
|
|
540
546
|
case 'incomplete': return `This change's last crawl stopped before finishing (${found.runId})`;
|
|
541
547
|
case 'cancelling': return `This change's crawl is being cancelled (${found.runId})`;
|
|
548
|
+
case 'prepare-only': return found.status === 'prepared'
|
|
549
|
+
? `Your change was built and frozen ahead of time (${found.runId})`
|
|
550
|
+
: `Your change is being built and frozen ahead of time (${found.runId})`;
|
|
542
551
|
}
|
|
543
552
|
}
|
|
544
553
|
/** Submits the capture through the explicit precompute submission when no crawl
|
|
@@ -584,7 +593,8 @@ async function submitCapture(capture, token, found) {
|
|
|
584
593
|
: `The service already had a crawl for this change (${crawl.runId}).` };
|
|
585
594
|
}
|
|
586
595
|
if (!anotherCrawl(found.stale) && crawl.runId === found.view.runId) {
|
|
587
|
-
return { runId: crawl.runId, onboarding: null, line:
|
|
596
|
+
return { runId: crawl.runId, onboarding: null, line: found.stale === 'prepare-only'
|
|
597
|
+
? `${before}; checking it now, reusing what was built.` : `${before}; asked the service to run it again.` };
|
|
588
598
|
}
|
|
589
599
|
return { runId: crawl.runId, onboarding: null, line: crawl.status === 'queued'
|
|
590
600
|
? `${before}; started one for this change (${crawl.runId}).`
|
|
@@ -619,7 +629,7 @@ export async function verifyCommand(options) {
|
|
|
619
629
|
console.error(line);
|
|
620
630
|
else
|
|
621
631
|
console.log(line); };
|
|
622
|
-
const capture = await captureCheckout(Date.now() + EXPLICIT_WALL_MS, repositoryOverride(options.repo), minutesOption(options.minutes));
|
|
632
|
+
const capture = await captureCheckout(Date.now() + EXPLICIT_WALL_MS, 'crawl', repositoryOverride(options.repo), minutesOption(options.minutes));
|
|
623
633
|
const request = capture.derivation.request;
|
|
624
634
|
const change = checkedChange(request.baseCommit, request.workCommit);
|
|
625
635
|
if (!request.crawl) {
|
package/dist/index.js
CHANGED
|
@@ -153,7 +153,8 @@ repository already keeps working. init shows each change as a diff, then makes
|
|
|
153
153
|
it with --yes (or a yes at the prompt):
|
|
154
154
|
• Claude Code's Stop hook (\`haystack verify precompute --hook\`) in the
|
|
155
155
|
per-developer .claude/settings.local.json, kept out of commits, when
|
|
156
|
-
Claude Code is in use here
|
|
156
|
+
Claude Code is in use here: it builds and freezes the changed app ahead
|
|
157
|
+
of time, so \`haystack verify\` starts from there
|
|
157
158
|
• a short note in AGENTS.md (and CLAUDE.md when the repository keeps one)
|
|
158
159
|
telling coding agents to run \`haystack verify\` after a change
|
|
159
160
|
Then it starts onboarding the app, so it is usually ready by the first
|
|
@@ -264,10 +265,9 @@ Exit codes:
|
|
|
264
265
|
0 the crawl answered or finished (the bugs it found are in the output), or
|
|
265
266
|
it is still running and --no-wait was given
|
|
266
267
|
1 the command failed, or no crawl could be started for this change
|
|
267
|
-
2 the crawl ended without finishing: it stopped early or was cancelled
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
before finishing
|
|
268
|
+
2 the crawl ended without finishing: it stopped early or was cancelled,
|
|
269
|
+
or the machines it used could not be proven shut down; or onboarding
|
|
270
|
+
stopped before finishing
|
|
271
271
|
3 onboarding is blocked: the output says what would unblock it
|
|
272
272
|
|
|
273
273
|
The hosted fleet run of pushed commits is \`haystack verify hosted start\`.
|
|
@@ -367,7 +367,7 @@ Examples:
|
|
|
367
367
|
});
|
|
368
368
|
verify
|
|
369
369
|
.command('precompute')
|
|
370
|
-
.description('
|
|
370
|
+
.description('Build and freeze the current working tree\'s app ahead of `haystack verify` (no crawl)')
|
|
371
371
|
.option('--hook', 'Run silently as a best-effort repository hook')
|
|
372
372
|
.action(async (options) => {
|
|
373
373
|
await runPublicCommand(async () => {
|
|
@@ -1359,7 +1359,8 @@ hooks
|
|
|
1359
1359
|
Configures your coding CLI to run \`haystack triage --hook\` on session start.
|
|
1360
1360
|
This shows pending PR analysis results when you open a new terminal session.
|
|
1361
1361
|
Claude Code also runs \`haystack verify precompute --hook\` when a session stops,
|
|
1362
|
-
which
|
|
1362
|
+
which builds and freezes the changed app in the background so that
|
|
1363
|
+
\`haystack verify\` starts from there. It never crawls; \`haystack verify\` does.
|
|
1363
1364
|
|
|
1364
1365
|
Claude Code: Native SessionStart and Stop hooks (.claude/settings.local.json;
|
|
1365
1366
|
--shared writes the committed .claude/settings.json)
|