@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.
- package/README.md +42 -2
- package/dist/commands/verify-core.d.ts +449 -0
- package/dist/commands/verify-core.js +789 -0
- package/dist/commands/verify-hosted.d.ts +60 -0
- package/dist/commands/verify-hosted.js +342 -0
- package/dist/commands/verify-mcp.d.ts +8 -0
- package/dist/commands/verify-mcp.js +517 -0
- package/dist/commands/verify-ops.d.ts +158 -0
- package/dist/commands/verify-ops.js +1148 -0
- package/dist/commands/verify-reproducibility.d.ts +85 -0
- package/dist/commands/verify-reproducibility.js +494 -0
- package/dist/commands/verify-reseal.d.ts +9 -0
- package/dist/commands/verify-reseal.js +148 -0
- package/dist/commands/verify-sandboxes.d.ts +95 -0
- package/dist/commands/verify-sandboxes.js +352 -0
- package/dist/commands/verify.d.ts +22 -0
- package/dist/commands/verify.js +130 -0
- package/dist/index.js +326 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.js +15 -0
- package/dist/utils/haystack-api.d.ts +1 -1
- package/dist/utils/haystack-api.js +2 -2
- package/package.json +2 -2
- package/schemas/cloud-verifier.v1.json +96 -0
|
@@ -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
package/dist/schema.js
CHANGED
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.
|
|
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": "
|
|
7
|
+
"haystack": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "rm -rf dist && tsc && cp -r src/assets dist/assets",
|