@haystackeditor/cli 0.15.20 → 0.15.22

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.
@@ -0,0 +1,130 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { spawn } from 'node:child_process';
3
+ import { resolve } from 'node:path';
4
+ import chalk from 'chalk';
5
+ import { setVerifyToken, verifyAuthHeaders } from './verify-core.js';
6
+ const DEFAULT_CONTROL_PLANE = 'http://127.0.0.1:3000';
7
+ export const VERIFY_SPECIMENS = new Set(['all', 'posthog-js-478', 'analytics-js-255']);
8
+ export function parseStartRunResponse(raw) {
9
+ let parsed;
10
+ try {
11
+ parsed = JSON.parse(raw);
12
+ }
13
+ catch {
14
+ throw new Error('Verifier returned an invalid JSON response.');
15
+ }
16
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
17
+ throw new Error('Verifier returned an invalid response object.');
18
+ }
19
+ const response = parsed;
20
+ const requiredString = (field) => {
21
+ const value = response[field];
22
+ if (typeof value !== 'string' || value.trim() === '') {
23
+ throw new Error(`Verifier response is missing a valid ${field}.`);
24
+ }
25
+ return value;
26
+ };
27
+ return {
28
+ runId: requiredString('runId'),
29
+ status: requiredString('status'),
30
+ resultsUrl: requiredString('resultsUrl'),
31
+ };
32
+ }
33
+ export function verifierErrorReason(raw, contentType) {
34
+ if (!contentType?.toLowerCase().includes('application/json'))
35
+ return raw;
36
+ let parsed;
37
+ try {
38
+ parsed = JSON.parse(raw);
39
+ }
40
+ catch (error) {
41
+ return `malformed JSON error response (${error instanceof Error ? error.message : String(error)}); raw body: ${raw}`;
42
+ }
43
+ if (parsed
44
+ && typeof parsed === 'object'
45
+ && !Array.isArray(parsed)
46
+ && typeof parsed.error === 'string'
47
+ && parsed.error.trim() !== '') {
48
+ return parsed.error;
49
+ }
50
+ return `JSON error response omitted a string error; raw body: ${raw}`;
51
+ }
52
+ function openResults(url) {
53
+ const platform = process.platform;
54
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
55
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
56
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
57
+ child.unref();
58
+ }
59
+ async function readIntentTranscript(path) {
60
+ if (!path)
61
+ return undefined;
62
+ const absolutePath = resolve(path);
63
+ try {
64
+ return await readFile(absolutePath, 'utf8');
65
+ }
66
+ catch (error) {
67
+ const message = error instanceof Error ? error.message : String(error);
68
+ throw new Error(`Could not read intent transcript at ${absolutePath}: ${message}`);
69
+ }
70
+ }
71
+ export async function startVerificationRun(specimenArg, options) {
72
+ const specimen = specimenArg || 'all';
73
+ if (!VERIFY_SPECIMENS.has(specimen)) {
74
+ throw new Error(`Unknown or unreproduced specimen "${specimen}". Choose all, posthog-js-478, or analytics-js-255.`);
75
+ }
76
+ setVerifyToken(options.token);
77
+ const server = (options.server || process.env.HAYSTACK_VERIFY_SERVER || DEFAULT_CONTROL_PLANE)
78
+ .replace(/\/+$/, '');
79
+ const intentTranscript = await readIntentTranscript(options.intentTranscript);
80
+ let response;
81
+ try {
82
+ response = await fetch(`${server}/api/cloud-verifier/runs`, {
83
+ method: 'POST',
84
+ headers: { 'content-type': 'application/json', ...verifyAuthHeaders() },
85
+ body: JSON.stringify({
86
+ specimen,
87
+ repoPath: options.repo ? resolve(options.repo) : undefined,
88
+ baseRef: options.base,
89
+ headRef: options.head,
90
+ intentTranscript,
91
+ planSourceRunId: options.planSourceRunId,
92
+ }),
93
+ });
94
+ }
95
+ catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ throw new Error(`Could not reach the Haystack verifier at ${server}. Start the demo control plane with \`pnpm dev\` and retry. ${message}`);
98
+ }
99
+ const raw = await response.text();
100
+ if (response.status === 401) {
101
+ throw new Error(`Control plane at ${server} requires a token (HTTP 401). Pass --token or set HAYSTACK_VERIFY_TOKEN.`);
102
+ }
103
+ if (!response.ok) {
104
+ const reason = verifierErrorReason(raw, response.headers.get('content-type'));
105
+ throw new Error(`Verifier rejected the run (${response.status}): ${reason}`);
106
+ }
107
+ return parseStartRunResponse(raw);
108
+ }
109
+ export async function verifyCommand(specimenArg, options) {
110
+ if (!options.json) {
111
+ console.error(chalk.dim('Preparing the cloud verification run…'));
112
+ }
113
+ const started = await startVerificationRun(specimenArg, options);
114
+ if (options.json) {
115
+ process.stdout.write(`${JSON.stringify({
116
+ schema_version: '1.0.0',
117
+ run_id: started.runId,
118
+ status: started.status,
119
+ results_url: started.resultsUrl,
120
+ }, null, 2)}\n`);
121
+ }
122
+ else {
123
+ console.log('');
124
+ console.log(`${chalk.green('✓')} Verification started: ${chalk.bold(started.runId)}`);
125
+ console.log(` ${chalk.dim('Results:')} ${chalk.cyan(started.resultsUrl)}`);
126
+ }
127
+ if (options.open !== false) {
128
+ openResults(started.resultsUrl);
129
+ }
130
+ }
package/dist/index.js CHANGED
@@ -48,6 +48,7 @@ import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
48
48
  import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
49
49
  import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
50
50
  import { systemMapValidateCommand } from './commands/system-map.js';
51
+ import { verifyCommand } from './commands/verify.js';
51
52
  import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
52
53
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
53
54
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
@@ -164,6 +165,331 @@ program
164
165
  .command('status')
165
166
  .description('Check if .haystack.json exists and is valid')
166
167
  .action(statusCommand);
168
+ const verify = program
169
+ .command('verify')
170
+ .description('Verify a change in matched cloud universes, or bisect a historical regression')
171
+ .argument('[specimen]', 'Launch specimen to run (default: all)')
172
+ .option('--repo <path>', 'Repository to analyze (defaults to the selected launch specimen)')
173
+ .option('--base <ref>', 'Base revision for a repository run')
174
+ .option('--head <ref>', 'Head revision for a repository run')
175
+ .option('--intent-transcript <file>', 'Coding-agent transcript used as intent evidence')
176
+ .option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
177
+ .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
178
+ .option('--no-open', 'Do not open the live results page')
179
+ .option('--json', 'Machine-readable run id, status, and results URL')
180
+ .addHelpText('after', `
181
+ Launch specimens:
182
+ posthog-js-478 Replay PostHog/posthog-js PR #478 in matched browser environments (latent-issue mode)
183
+ analytics-js-255 Identity routing regression with sandbox bisect
184
+ all Run each reproduced specimen as an isolated chapter (default)
185
+
186
+ The local control plane must be running. From the Haystack repository:
187
+ pnpm dev
188
+
189
+ Examples:
190
+ haystack verify hosted start owner/repo --base <sha> --head <sha>
191
+ haystack verify posthog-js-478 # start a run, then:
192
+ haystack verify reproducibility posthog-js-478 --runs 3
193
+ haystack verify watch # block until it finishes (exit 1 = tests failed)
194
+ haystack verify status # one-screen verdicts + live environment URLs
195
+ haystack verify findings # every problem found, with the readings that moved
196
+ haystack verify show latest 3 # one test in full: readings, timeline, SDK log
197
+ haystack verify diff latest 3 # base-vs-head screenshot differ (1/2/3 views)
198
+ haystack verify open latest 3 --live # open a failing test's live environment
199
+ haystack verify cleanup latest 3 # retained-state deletion/reference status
200
+ haystack verify refork latest 3 attempt:fix-2 # checkpoint a durable child
201
+ haystack verify shell latest 3 # terminal into that live universe
202
+ haystack verify artifacts latest # pull screenshots / DOM / video locally
203
+ haystack verify sandboxes --kill-stale # reclaim E2B capacity from superseded runs
204
+ haystack verify reseal <specimen> # rebuild a sealed bundle + restart control plane
205
+ `)
206
+ .action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
207
+ const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
208
+ const hostedVerify = verify
209
+ .command('hosted')
210
+ .description('Run the production Cloud Verifier against exact GitHub commits');
211
+ hostedVerify
212
+ .command('start')
213
+ .description('Start an authenticated production verification and wait for its terminal result')
214
+ .argument('<repository>', 'Exact GitHub owner/repository name')
215
+ // `verify` already owns --base/--head for the local specimen launcher.
216
+ // Re-declaring these as Commander requiredOption values makes the parent
217
+ // consume them and the child then reject a valid invocation. Runtime
218
+ // validation below accepts the inherited values and still fails closed.
219
+ .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
220
+ .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
221
+ .option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
222
+ .option('--account <login>', 'Use a specific saved Haystack account')
223
+ .option('--no-wait', 'Return after the production workflow is queued')
224
+ .option('--interval <seconds>', 'Polling interval while waiting (default 5)')
225
+ .option('--timeout <minutes>', 'Maximum wait (default 35)')
226
+ .option('--json', 'Versioned machine-readable run status and terminal output')
227
+ .addHelpText('after', `
228
+ The default idempotency key makes an exact retry return the original run. Pass
229
+ --idempotency-key with a new stable key only when you intentionally want a
230
+ fresh production execution of the same commit pair.
231
+
232
+ Examples:
233
+ haystack verify hosted start owner/repo --base <sha> --head <sha>
234
+ haystack verify hosted start owner/repo --base <sha> --head <sha> --no-wait --json
235
+ `)
236
+ .action(async (repository, _options, cmd) => {
237
+ const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
238
+ const { verifyHostedStartCommand } = await import('./commands/verify-hosted.js');
239
+ return runPublicCommand(() => verifyHostedStartCommand(repository, options), options.json);
240
+ });
241
+ hostedVerify
242
+ .command('status')
243
+ .description('Read an owned production verification, optionally waiting until terminal')
244
+ .argument('<run-id>', 'Hosted run id (cv_ followed by 48 lowercase hex characters)')
245
+ .option('--account <login>', 'Use a specific saved Haystack account')
246
+ .option('--wait', 'Keep polling until the run reaches a terminal status')
247
+ .option('--interval <seconds>', 'Polling interval while waiting (default 5)')
248
+ .option('--timeout <minutes>', 'Maximum wait (default 35)')
249
+ .option('--json', 'Versioned machine-readable run status and terminal output')
250
+ .addHelpText('after', `
251
+ Example:
252
+ haystack verify hosted status cv_<48 lowercase hex characters> --wait --json
253
+ `)
254
+ .action(async (runId, _options, cmd) => {
255
+ const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
256
+ const { verifyHostedStatusCommand } = await import('./commands/verify-hosted.js');
257
+ return runPublicCommand(() => verifyHostedStatusCommand(runId, options), options.json);
258
+ });
259
+ verify
260
+ .command('reproducibility')
261
+ .description('Separate frozen-plan drift from executor/judgment nondeterminism')
262
+ .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
263
+ .option('--runs <n>', 'Number of sequential runs to compare (default 3; min 2, max 20)')
264
+ .option('--scope <scope>', 'executor replays one frozen plan (default); planner/full independently replan each run', 'executor')
265
+ .option('--plan-source <runId>', 'executor scope: replay this existing structured frozen plan for every run')
266
+ .option('--interval <seconds>', 'Polling interval for each run (default 3)')
267
+ .option('--timeout <minutes>', 'Per-run timeout (default: wait forever)')
268
+ .option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
269
+ .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
270
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
271
+ .option('--json', 'Machine-readable stable/unstable cell report')
272
+ .action(async (specimen, commandOptions, cmd) => {
273
+ const options = { ...cmd.optsWithGlobals(), ...commandOptions };
274
+ const { verifyReproducibilityCommand } = await import('./commands/verify-reproducibility.js');
275
+ return runPublicCommand(() => verifyReproducibilityCommand(specimen, options), options.json);
276
+ });
277
+ verify
278
+ .command('status')
279
+ .description('One screen for a run: per-test verdicts, counts, live environments and expiry')
280
+ .argument('[run]', verifyRunArg)
281
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
282
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
283
+ .option('--json', 'Machine-readable summary')
284
+ .action(async (run, _options, cmd) => {
285
+ // optsWithGlobals: the parent `verify` command also declares --json/--server
286
+ // and commander's default parsing lets it consume those flags even when
287
+ // they appear after the subcommand; the merged view (self wins) restores them.
288
+ const options = cmd.optsWithGlobals();
289
+ const { verifyStatusCommand } = await import('./commands/verify-ops.js');
290
+ return runPublicCommand(() => verifyStatusCommand(run, options), options.json);
291
+ });
292
+ verify
293
+ .command('watch')
294
+ .description('Follow a run until it finishes; exit 0 all passed, 1 tests failed, 2 run/infra broke')
295
+ .argument('[run]', verifyRunArg)
296
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
297
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
298
+ .option('--interval <seconds>', 'Poll interval (default 3)')
299
+ .option('--timeout <minutes>', 'Give up after this long (default: wait forever)')
300
+ .option('--json', 'Suppress streaming; print the final summary as JSON')
301
+ .action(async (run, _options, cmd) => {
302
+ const options = cmd.optsWithGlobals();
303
+ const { verifyWatchCommand } = await import('./commands/verify-ops.js');
304
+ return runPublicCommand(() => verifyWatchCommand(run, options), options.json);
305
+ });
306
+ verify
307
+ .command('list')
308
+ .description('Runs newest first: chapter, verdict counts, age (control plane, else disk)')
309
+ .option('--limit <n>', 'Max runs to show (default 20)')
310
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
311
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
312
+ .option('--json', 'Machine-readable list')
313
+ .action(async (_options, cmd) => {
314
+ const options = cmd.optsWithGlobals();
315
+ const { verifyListCommand } = await import('./commands/verify-ops.js');
316
+ return runPublicCommand(() => verifyListCommand(options), options.json);
317
+ });
318
+ verify
319
+ .command('findings')
320
+ .description('Every problem the run found, in one screen: what failed, which readings moved, live links')
321
+ .argument('[run]', verifyRunArg)
322
+ .option('--full', 'Print the judge\'s full explanation for each finding')
323
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
324
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
325
+ .option('--json', 'Machine-readable findings')
326
+ .action(async (run, _options, cmd) => {
327
+ const options = cmd.optsWithGlobals();
328
+ const { verifyFindingsCommand } = await import('./commands/verify-ops.js');
329
+ return runPublicCommand(() => verifyFindingsCommand(run, options), options.json);
330
+ });
331
+ verify
332
+ .command('open')
333
+ .description('Open a run artifact (screenshot/video/DOM) or a failing test\'s live environment')
334
+ .argument('<run>', verifyRunArg)
335
+ .argument('<target>', 'Test selector (ordinal/title), or an artifact file name (….png/.webm/.html)')
336
+ .option('--live', 'Open the test\'s live environment URL instead of an artifact')
337
+ .option('--side <side>', 'base or head (default head)')
338
+ .option('--step <n>', 'Open the screenshot captured after replay step n')
339
+ .option('--kind <kind>', 'screenshot | video | dom (default: video, else last screenshot)')
340
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
341
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
342
+ .action(async (run, target, _options, cmd) => {
343
+ const options = cmd.optsWithGlobals();
344
+ const { verifyOpenCommand } = await import('./commands/verify-ops.js');
345
+ return runPublicCommand(() => verifyOpenCommand(run, target, options));
346
+ });
347
+ verify
348
+ .command('diff')
349
+ .description('Open a base-vs-head screenshot differ for one test (1 what changed / 2 before / 3 after)')
350
+ .argument('<run>', verifyRunArg)
351
+ .argument('<cell>', 'Test selector: ordinal, cell id, or title substring')
352
+ .option('--step <n>', 'Compare only the screenshot captured after replay step n')
353
+ .option('--out <dir>', 'Where to build the diff page (default: a temp directory)')
354
+ .option('--no-open', 'Build the page but do not open it')
355
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
356
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
357
+ .option('--json', 'Machine-readable result (implies --no-open)')
358
+ .action(async (run, cell, _options, cmd) => {
359
+ const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
360
+ const { verifyDiffCommand } = await import('./commands/verify-ops.js');
361
+ return runPublicCommand(() => verifyDiffCommand(run, cell, options), options.json);
362
+ });
363
+ verify
364
+ .command('cleanup')
365
+ .description('Show exact retained-state references, provider deletion, and cleanup-SLO status')
366
+ .argument('<run>', verifyRunArg)
367
+ .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
368
+ .option('--side <side>', 'base or head (default head)')
369
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
370
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
371
+ .option('--json', 'Machine-readable cleanup status')
372
+ .action(async (run, cell, _options, cmd) => {
373
+ const options = cmd.optsWithGlobals();
374
+ const { verifyCleanupCommand } = await import('./commands/verify-ops.js');
375
+ return runPublicCommand(() => verifyCleanupCommand(run, cell, options), options.json);
376
+ });
377
+ verify
378
+ .command('reopen')
379
+ .description('Attach fresh E2B compute to retained Archil state from a test or exact fork ID')
380
+ .argument('<source>', 'Run selector, or an exact archil:…:branch:… fork ID')
381
+ .argument('[cell]', 'Stable test selector for a run: ordinal, riskCellId, or exact planner cellKey')
382
+ .option('--side <side>', 'base or head (default head)')
383
+ .option('--open-browser', 'Open the restarted application in a browser')
384
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
385
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
386
+ .option('--json', 'Machine-readable attachment details')
387
+ .action(async (source, cell, _options, cmd) => {
388
+ const options = cmd.optsWithGlobals();
389
+ const { verifyReopenCommand } = await import('./commands/verify-ops.js');
390
+ return runPublicCommand(() => verifyReopenCommand(source, cell, options), options.json);
391
+ });
392
+ verify
393
+ .command('refork')
394
+ .description('Checkpoint a reopened Archil universe into a durable child fork')
395
+ .argument('<source>', 'Run selector, or an exact archil:…:branch:… parent fork ID')
396
+ .argument('<cell-or-child-key>', 'Stable test selector for a run, or child key for an exact fork ID')
397
+ .argument('[child-key]', 'Structured stable child key when source is a run')
398
+ .option('--side <side>', 'base or head (default head; only applies to a run)')
399
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
400
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
401
+ .option('--json', 'Machine-readable parent, child, checkpoint, and expiry')
402
+ .action(async (source, cellOrChildKey, childKey, _options, cmd) => {
403
+ const options = cmd.optsWithGlobals();
404
+ const { verifyReforkCommand } = await import('./commands/verify-ops.js');
405
+ return runPublicCommand(() => verifyReforkCommand(source, cellOrChildKey, childKey, options), options.json);
406
+ });
407
+ verify
408
+ .command('shell')
409
+ .description('Terminal into a failing test\'s live universe (e2b sandbox connect)')
410
+ .argument('<run>', verifyRunArg)
411
+ .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
412
+ .option('--side <side>', 'base or head (default head; only failing tests keep a universe alive)')
413
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
414
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
415
+ .action(async (run, cell, _options, cmd) => {
416
+ const options = cmd.optsWithGlobals();
417
+ const { verifyShellCommand } = await import('./commands/verify-ops.js');
418
+ return runPublicCommand(() => verifyShellCommand(run, cell, options));
419
+ });
420
+ verify
421
+ .command('show')
422
+ .description('One test in full: finding, changed readings, request timeline, SDK log lines')
423
+ .argument('<run>', verifyRunArg)
424
+ .argument('<cell>', 'Test ordinal, cell id, planner cell key, or title substring')
425
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
426
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
427
+ .option('--no-timeline', 'Skip the request timeline')
428
+ .option('--json', 'Machine-readable detail')
429
+ .action(async (run, cell, _options, cmd) => {
430
+ const options = cmd.optsWithGlobals();
431
+ const { verifyShowCommand } = await import('./commands/verify-ops.js');
432
+ return runPublicCommand(() => verifyShowCommand(run, cell, options), options.json);
433
+ });
434
+ verify
435
+ .command('artifacts')
436
+ .description('Pull a run\'s screenshots, DOM snapshots and videos to a local directory')
437
+ .argument('<run>', verifyRunArg)
438
+ .option('--out <dir>', 'Destination (default: ./verify-artifacts/<runId>)')
439
+ .option('--cell <selector>', 'Only artifacts for one test (ordinal, cell id, or title substring)')
440
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
441
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
442
+ .option('--json', 'Machine-readable result')
443
+ .action(async (run, _options, cmd) => {
444
+ const options = cmd.optsWithGlobals();
445
+ const { verifyArtifactsCommand } = await import('./commands/verify-ops.js');
446
+ return runPublicCommand(() => verifyArtifactsCommand(run, options), options.json);
447
+ });
448
+ verify
449
+ .command('sandboxes')
450
+ .description('E2B sandbox inventory with run attribution; --kill-stale reclaims superseded ones')
451
+ .option('--kill-stale', 'Kill verifier-attributed sandboxes superseded by newer runs (never unattributed ones)')
452
+ .option('--kill-run <runId>', 'Kill every sandbox attributed to one run')
453
+ .option('--server <url>', 'Control-plane URL, preferred over direct E2B access (default: http://127.0.0.1:3000)')
454
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
455
+ .option('--json', 'Machine-readable inventory')
456
+ .action(async (_options, cmd) => {
457
+ const options = cmd.optsWithGlobals();
458
+ const { verifySandboxesCommand } = await import('./commands/verify-sandboxes.js');
459
+ return runPublicCommand(() => verifySandboxesCommand(options), options.json);
460
+ });
461
+ verify
462
+ .command('reseal')
463
+ .description('Rebuild a sealed specimen bundle (guarded) and restart the control plane onto it')
464
+ .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
465
+ .option('--source <dir>', 'Specimen source directory holding reseal.sh')
466
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
467
+ .option('--repo-root <path>', 'Repository holding demo/cloud-verifier (default: walk up from cwd)')
468
+ .option('--no-restart', 'Only rebuild the bundle; leave the running server on the old checkout')
469
+ .action(async (specimen, _options, cmd) => {
470
+ const options = cmd.optsWithGlobals();
471
+ const { verifyResealCommand } = await import('./commands/verify-reseal.js');
472
+ return runPublicCommand(() => verifyResealCommand(specimen, options));
473
+ });
474
+ verify
475
+ .command('mcp')
476
+ .description('Run a stdio MCP server exposing the verifier to agents (verify_start/verify_wait/…)')
477
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
478
+ .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
479
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
480
+ .action(async (_options, cmd) => {
481
+ const options = cmd.optsWithGlobals();
482
+ try {
483
+ // Lazy import, same reasoning as the top-level `mcp` command: only this
484
+ // path needs @modelcontextprotocol/sdk.
485
+ const { runVerifyMcpServer } = await import('./commands/verify-mcp.js');
486
+ await runVerifyMcpServer(options);
487
+ }
488
+ catch (err) {
489
+ console.error(chalk.red('verify mcp server failed:'), err instanceof Error ? err.message : err);
490
+ process.exit(1);
491
+ }
492
+ });
167
493
  program
168
494
  .command('login')
169
495
  .description('Add or refresh a saved GitHub account (use --headless for CI tokens)')
package/dist/schema.d.ts CHANGED
@@ -19,6 +19,7 @@ export declare const SCHEMA_VERSIONS: {
19
19
  readonly traces: "1.0.0";
20
20
  readonly submit: "1.0.0";
21
21
  readonly action: "1.0.0";
22
+ readonly 'cloud-verifier': "1.0.0";
22
23
  readonly error: "1.0.0";
23
24
  };
24
25
  export type SchemaName = keyof typeof SCHEMA_VERSIONS;
package/dist/schema.js CHANGED
@@ -19,6 +19,7 @@ export const SCHEMA_VERSIONS = {
19
19
  traces: '1.0.0',
20
20
  submit: '1.0.0',
21
21
  action: '1.0.0',
22
+ 'cloud-verifier': '1.0.0',
22
23
  error: '1.0.0',
23
24
  };
24
25
  /** Wrap a payload with the `schema_version` envelope. */
package/dist/types.d.ts CHANGED
@@ -719,11 +719,52 @@ declare const TriageConfigSchema: z.ZodObject<{
719
719
  maxTurns?: Partial<Record<"code-review" | "rules-validator" | "intent-drift", number>> | undefined;
720
720
  timeoutMs?: number | undefined;
721
721
  }>;
722
+ declare const PreferencesSchema: z.ZodObject<{
723
+ /** Run analysis automatically for PR webhook events (default: true) */
724
+ automatic_analysis: z.ZodOptional<z.ZodBoolean>;
725
+ /** Enable sandbox-backed verification for this repository */
726
+ sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
727
+ /** Enroll submitted PRs in auto-merge by default */
728
+ auto_merge: z.ZodOptional<z.ZodBoolean>;
729
+ /** Allow Haystack to attempt automatic fixes */
730
+ auto_fix: z.ZodOptional<z.ZodBoolean>;
731
+ }, "strip", z.ZodTypeAny, {
732
+ automatic_analysis?: boolean | undefined;
733
+ sandbox_enabled?: boolean | undefined;
734
+ auto_merge?: boolean | undefined;
735
+ auto_fix?: boolean | undefined;
736
+ }, {
737
+ automatic_analysis?: boolean | undefined;
738
+ sandbox_enabled?: boolean | undefined;
739
+ auto_merge?: boolean | undefined;
740
+ auto_fix?: boolean | undefined;
741
+ }>;
722
742
  export declare const HaystackConfigSchema: z.ZodObject<{
723
743
  /** Config version (must be "1") */
724
744
  version: z.ZodLiteral<"1">;
725
745
  /** Project name (for display) */
726
746
  name: z.ZodOptional<z.ZodString>;
747
+ /** Repository-level Haystack behavior */
748
+ preferences: z.ZodOptional<z.ZodObject<{
749
+ /** Run analysis automatically for PR webhook events (default: true) */
750
+ automatic_analysis: z.ZodOptional<z.ZodBoolean>;
751
+ /** Enable sandbox-backed verification for this repository */
752
+ sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
753
+ /** Enroll submitted PRs in auto-merge by default */
754
+ auto_merge: z.ZodOptional<z.ZodBoolean>;
755
+ /** Allow Haystack to attempt automatic fixes */
756
+ auto_fix: z.ZodOptional<z.ZodBoolean>;
757
+ }, "strip", z.ZodTypeAny, {
758
+ automatic_analysis?: boolean | undefined;
759
+ sandbox_enabled?: boolean | undefined;
760
+ auto_merge?: boolean | undefined;
761
+ auto_fix?: boolean | undefined;
762
+ }, {
763
+ automatic_analysis?: boolean | undefined;
764
+ sandbox_enabled?: boolean | undefined;
765
+ auto_merge?: boolean | undefined;
766
+ auto_fix?: boolean | undefined;
767
+ }>>;
727
768
  /** Package manager override (auto-detected if not specified) */
728
769
  package_manager: z.ZodOptional<z.ZodEnum<["npm", "pnpm", "yarn", "bun"]>>;
729
770
  /** Development server configuration */
@@ -1315,6 +1356,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
1315
1356
  root?: string | undefined;
1316
1357
  depends_on?: string[] | undefined;
1317
1358
  }> | undefined;
1359
+ preferences?: {
1360
+ automatic_analysis?: boolean | undefined;
1361
+ sandbox_enabled?: boolean | undefined;
1362
+ auto_merge?: boolean | undefined;
1363
+ auto_fix?: boolean | undefined;
1364
+ } | undefined;
1318
1365
  package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
1319
1366
  dev_server?: {
1320
1367
  command: string;
@@ -1442,6 +1489,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
1442
1489
  root?: string | undefined;
1443
1490
  depends_on?: string[] | undefined;
1444
1491
  }> | undefined;
1492
+ preferences?: {
1493
+ automatic_analysis?: boolean | undefined;
1494
+ sandbox_enabled?: boolean | undefined;
1495
+ auto_merge?: boolean | undefined;
1496
+ auto_fix?: boolean | undefined;
1497
+ } | undefined;
1445
1498
  package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
1446
1499
  dev_server?: {
1447
1500
  command: string;
@@ -1559,6 +1612,7 @@ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;
1559
1612
  export type SecretDeclaration = z.infer<typeof SecretDeclarationSchema>;
1560
1613
  export type MergeQueueConfig = z.infer<typeof MergeQueueConfigSchema>;
1561
1614
  export type TriageConfig = z.infer<typeof TriageConfigSchema>;
1615
+ export type Preferences = z.infer<typeof PreferencesSchema>;
1562
1616
  /**
1563
1617
  * Project detection results
1564
1618
  */
package/dist/types.js CHANGED
@@ -280,6 +280,19 @@ const TriageConfigSchema = z.object({
280
280
  timeoutMs: z.number().int().positive().optional(),
281
281
  });
282
282
  // =============================================================================
283
+ // REPOSITORY PREFERENCES
284
+ // =============================================================================
285
+ const PreferencesSchema = z.object({
286
+ /** Run analysis automatically for PR webhook events (default: true) */
287
+ automatic_analysis: z.boolean().optional(),
288
+ /** Enable sandbox-backed verification for this repository */
289
+ sandbox_enabled: z.boolean().optional(),
290
+ /** Enroll submitted PRs in auto-merge by default */
291
+ auto_merge: z.boolean().optional(),
292
+ /** Allow Haystack to attempt automatic fixes */
293
+ auto_fix: z.boolean().optional(),
294
+ });
295
+ // =============================================================================
283
296
  // MAIN CONFIG
284
297
  // =============================================================================
285
298
  export const HaystackConfigSchema = z.object({
@@ -287,6 +300,8 @@ export const HaystackConfigSchema = z.object({
287
300
  version: z.literal('1'),
288
301
  /** Project name (for display) */
289
302
  name: z.string().optional(),
303
+ /** Repository-level Haystack behavior */
304
+ preferences: PreferencesSchema.optional(),
290
305
  /** Package manager override (auto-detected if not specified) */
291
306
  package_manager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
292
307
  /** Development server configuration */
@@ -27,7 +27,7 @@ export declare class HaystackApiError extends Error {
27
27
  * class it failed with, and the next command to run.
28
28
  */
29
29
  export declare function classifyHttpError(response: Response, context: string): Promise<HaystackApiError>;
30
- export declare function haystackJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
30
+ export declare function haystackJson<T>(path: string, token: string, init?: RequestInit, acceptedStatuses?: readonly number[]): Promise<T>;
31
31
  export declare function fetchCliIdentity(token: string): Promise<CliIdentity>;
32
32
  export declare function fetchHaystackInstallations(token: string): Promise<UserInstallation[]>;
33
33
  export declare function fetchGitHubJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
@@ -69,12 +69,12 @@ export async function classifyHttpError(response, context) {
69
69
  }
70
70
  return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
71
71
  }
72
- export async function haystackJson(path, token, init = {}) {
72
+ export async function haystackJson(path, token, init = {}, acceptedStatuses = []) {
73
73
  const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
74
74
  ...init,
75
75
  headers: headers(token, init.headers),
76
76
  });
77
- if (!response.ok) {
77
+ if (!response.ok && !acceptedStatuses.includes(response.status)) {
78
78
  throw await classifyHttpError(response, `Haystack API ${path}`);
79
79
  }
80
80
  return response.json();
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.20",
3
+ "version": "0.15.22",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
7
- "haystack": "./dist/index.js"
7
+ "haystack": "dist/index.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "rm -rf dist && tsc && cp -r src/assets dist/assets",