@haystackeditor/cli 0.15.19 → 0.15.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +100 -1
  2. package/dist/assets/skills/map-your-system.md +9 -4
  3. package/dist/commands/ask.js +2 -2
  4. package/dist/commands/config.d.ts +9 -0
  5. package/dist/commands/config.js +46 -14
  6. package/dist/commands/dismiss.d.ts +15 -3
  7. package/dist/commands/dismiss.js +66 -116
  8. package/dist/commands/mcp.js +96 -1
  9. package/dist/commands/policy.js +31 -12
  10. package/dist/commands/pr-status.d.ts +125 -2
  11. package/dist/commands/pr-status.js +88 -73
  12. package/dist/commands/pr.d.ts +1 -7
  13. package/dist/commands/pr.js +10 -21
  14. package/dist/commands/request-review.d.ts +12 -1
  15. package/dist/commands/request-review.js +42 -65
  16. package/dist/commands/review.d.ts +1 -0
  17. package/dist/commands/review.js +58 -60
  18. package/dist/commands/setup.js +4 -1
  19. package/dist/commands/submit.d.ts +3 -0
  20. package/dist/commands/submit.js +178 -38
  21. package/dist/commands/system-map.d.ts +42 -0
  22. package/dist/commands/system-map.js +228 -0
  23. package/dist/commands/tokens.js +3 -2
  24. package/dist/commands/traces.js +2 -2
  25. package/dist/commands/triage.d.ts +1 -6
  26. package/dist/commands/triage.js +51 -55
  27. package/dist/commands/verify-core.d.ts +424 -0
  28. package/dist/commands/verify-core.js +753 -0
  29. package/dist/commands/verify-mcp.d.ts +8 -0
  30. package/dist/commands/verify-mcp.js +458 -0
  31. package/dist/commands/verify-ops.d.ts +158 -0
  32. package/dist/commands/verify-ops.js +1148 -0
  33. package/dist/commands/verify-reseal.d.ts +9 -0
  34. package/dist/commands/verify-reseal.js +148 -0
  35. package/dist/commands/verify-sandboxes.d.ts +95 -0
  36. package/dist/commands/verify-sandboxes.js +352 -0
  37. package/dist/commands/verify.d.ts +11 -0
  38. package/dist/commands/verify.js +97 -0
  39. package/dist/index.js +343 -20
  40. package/dist/schema.d.ts +4 -0
  41. package/dist/schema.js +4 -0
  42. package/dist/states.d.ts +29 -0
  43. package/dist/states.js +46 -0
  44. package/dist/triage/runner.d.ts +7 -0
  45. package/dist/triage/runner.js +2 -2
  46. package/dist/types.d.ts +54 -0
  47. package/dist/types.js +15 -0
  48. package/dist/utils/action-output.d.ts +24 -0
  49. package/dist/utils/action-output.js +26 -0
  50. package/dist/utils/analysis-api.d.ts +11 -0
  51. package/dist/utils/analysis-api.js +29 -3
  52. package/dist/utils/auth.js +7 -0
  53. package/dist/utils/git.d.ts +4 -1
  54. package/dist/utils/git.js +17 -7
  55. package/dist/utils/haystack-api.d.ts +15 -0
  56. package/dist/utils/haystack-api.js +58 -7
  57. package/dist/utils/pr-ref.d.ts +27 -0
  58. package/dist/utils/pr-ref.js +56 -0
  59. package/package.json +2 -2
  60. package/schemas/action.v1.json +22 -0
  61. package/schemas/error.v1.json +14 -0
  62. package/schemas/pr-status.v1.json +61 -0
  63. package/schemas/pr.v1.json +80 -17
  64. package/schemas/setup.v1.json +66 -12
  65. package/schemas/submit.v1.json +88 -0
  66. package/schemas/triage.v1.json +71 -18
@@ -0,0 +1,97 @@
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
+ const SPECIMENS = new Set(['all', 'posthog-js-478', 'analytics-js-255']);
8
+ function openResults(url) {
9
+ const platform = process.platform;
10
+ const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
11
+ const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
12
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' });
13
+ child.unref();
14
+ }
15
+ async function readIntentTranscript(path) {
16
+ if (!path)
17
+ return undefined;
18
+ const absolutePath = resolve(path);
19
+ try {
20
+ return await readFile(absolutePath, 'utf8');
21
+ }
22
+ catch (error) {
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ throw new Error(`Could not read intent transcript at ${absolutePath}: ${message}`);
25
+ }
26
+ }
27
+ export async function verifyCommand(specimenArg, options) {
28
+ const specimen = specimenArg || 'all';
29
+ if (!SPECIMENS.has(specimen)) {
30
+ throw new Error(`Unknown or unreproduced specimen "${specimen}". Choose all, posthog-js-478, or analytics-js-255.`);
31
+ }
32
+ setVerifyToken(options.token);
33
+ const server = (options.server || process.env.HAYSTACK_VERIFY_SERVER || DEFAULT_CONTROL_PLANE)
34
+ .replace(/\/+$/, '');
35
+ const intentTranscript = await readIntentTranscript(options.intentTranscript);
36
+ if (!options.json) {
37
+ console.error(chalk.dim('Preparing the cloud verification run…'));
38
+ }
39
+ let response;
40
+ try {
41
+ response = await fetch(`${server}/api/cloud-verifier/runs`, {
42
+ method: 'POST',
43
+ headers: { 'content-type': 'application/json', ...verifyAuthHeaders() },
44
+ body: JSON.stringify({
45
+ specimen,
46
+ repoPath: options.repo ? resolve(options.repo) : undefined,
47
+ baseRef: options.base,
48
+ headRef: options.head,
49
+ intentTranscript,
50
+ }),
51
+ });
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ throw new Error(`Could not reach the Haystack verifier at ${server}. Start the demo control plane with \`pnpm dev\` and retry. ${message}`);
56
+ }
57
+ const raw = await response.text();
58
+ if (response.status === 401) {
59
+ throw new Error(`Control plane at ${server} requires a token (HTTP 401). Pass --token or set HAYSTACK_VERIFY_TOKEN.`);
60
+ }
61
+ if (!response.ok) {
62
+ let reason = raw;
63
+ try {
64
+ reason = JSON.parse(raw).error || raw;
65
+ }
66
+ catch {
67
+ // Preserve the response body when it is not JSON.
68
+ }
69
+ throw new Error(`Verifier rejected the run (${response.status}): ${reason}`);
70
+ }
71
+ let started;
72
+ try {
73
+ started = JSON.parse(raw);
74
+ }
75
+ catch {
76
+ throw new Error('Verifier returned an invalid response.');
77
+ }
78
+ if (!started.runId || !started.resultsUrl) {
79
+ throw new Error('Verifier response is missing runId or resultsUrl.');
80
+ }
81
+ if (options.json) {
82
+ process.stdout.write(`${JSON.stringify({
83
+ schema_version: '1.0.0',
84
+ run_id: started.runId,
85
+ status: started.status,
86
+ results_url: started.resultsUrl,
87
+ }, null, 2)}\n`);
88
+ }
89
+ else {
90
+ console.log('');
91
+ console.log(`${chalk.green('✓')} Verification started: ${chalk.bold(started.runId)}`);
92
+ console.log(` ${chalk.dim('Results:')} ${chalk.cyan(started.resultsUrl)}`);
93
+ }
94
+ if (options.open !== false) {
95
+ openResults(started.resultsUrl);
96
+ }
97
+ }
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ import { dirname, join } from 'node:path';
25
25
  import chalk from 'chalk';
26
26
  import { Command, Option } from 'commander';
27
27
  import { statusCommand } from './commands/status.js';
28
+ import { withSchema } from './schema.js';
28
29
  import { initCommand } from './commands/init.js';
29
30
  import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
30
31
  import { handleAgenticTool, handleAutoMerge, handleAutoFix, handleWaitForReviewers, isAutoMergeEnabled, isAutoFixEnabled } from './commands/config.js';
@@ -46,6 +47,8 @@ import { setupCommand } from './commands/setup.js';
46
47
  import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
47
48
  import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
48
49
  import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
50
+ import { systemMapValidateCommand } from './commands/system-map.js';
51
+ import { verifyCommand } from './commands/verify.js';
49
52
  import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
50
53
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
51
54
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
@@ -53,12 +56,19 @@ import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeT
53
56
  // like `triage`/`submit`. (0.15.12 shipped `mcp.js` but published its package.json
54
57
  // WITHOUT `@modelcontextprotocol/sdk`, so the eager import here hard-crashed every
55
58
  // command at startup with ERR_MODULE_NOT_FOUND.)
56
- async function runPublicCommand(action) {
59
+ async function runPublicCommand(action, json) {
57
60
  try {
58
61
  await action();
59
62
  }
60
63
  catch (err) {
61
- console.error(chalk.red(err instanceof Error ? err.message : String(err)));
64
+ const message = err instanceof Error ? err.message : String(err);
65
+ // In --json mode stdout must still carry one parseable document — emit the
66
+ // versioned error envelope (`haystack schema error`) instead of leaving
67
+ // stdout empty with prose on stderr.
68
+ if (json) {
69
+ process.stdout.write(JSON.stringify(withSchema('error', { status: 'error', error: message }), null, 2) + '\n');
70
+ }
71
+ console.error(chalk.red(message));
62
72
  process.exitCode = 1;
63
73
  }
64
74
  }
@@ -112,7 +122,7 @@ program
112
122
  .description('Onboarding wizard — scan repos for rules/policies and write Haystack config')
113
123
  .option('--json', 'Agent mode: emit NDJSON events on stdout, read answers on stdin (no TTY prompts)')
114
124
  .option('--repo <repo...>', 'Repo(s) to configure (owner/name); skips the repo picker')
115
- .option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write)')
125
+ .option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write, ENABLE auto-merge — pass --no-auto-merge to opt out)')
116
126
  // Defining --no-auto-merge also accepts --auto-merge; default is "ask" unless
117
127
  // one is explicitly passed (detected via getOptionValueSource below).
118
128
  .option('--no-auto-merge', 'Set auto-merge in the written config (--auto-merge / --no-auto-merge; omit to be asked)')
@@ -155,6 +165,260 @@ program
155
165
  .command('status')
156
166
  .description('Check if .haystack.json exists and is valid')
157
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 posthog-js-478 # start a run, then:
191
+ haystack verify watch # block until it finishes (exit 1 = tests failed)
192
+ haystack verify status # one-screen verdicts + live environment URLs
193
+ haystack verify findings # every problem found, with the readings that moved
194
+ haystack verify show latest 3 # one test in full: readings, timeline, SDK log
195
+ haystack verify diff latest 3 # base-vs-head screenshot differ (1/2/3 views)
196
+ haystack verify open latest 3 --live # open a failing test's live environment
197
+ haystack verify cleanup latest 3 # retained-state deletion/reference status
198
+ haystack verify refork latest 3 attempt:fix-2 # checkpoint a durable child
199
+ haystack verify shell latest 3 # terminal into that live universe
200
+ haystack verify artifacts latest # pull screenshots / DOM / video locally
201
+ haystack verify sandboxes --kill-stale # reclaim E2B capacity from superseded runs
202
+ haystack verify reseal <specimen> # rebuild a sealed bundle + restart control plane
203
+ `)
204
+ .action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
205
+ const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
206
+ verify
207
+ .command('status')
208
+ .description('One screen for a run: per-test verdicts, counts, live environments and expiry')
209
+ .argument('[run]', verifyRunArg)
210
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
211
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
212
+ .option('--json', 'Machine-readable summary')
213
+ .action(async (run, _options, cmd) => {
214
+ // optsWithGlobals: the parent `verify` command also declares --json/--server
215
+ // and commander's default parsing lets it consume those flags even when
216
+ // they appear after the subcommand; the merged view (self wins) restores them.
217
+ const options = cmd.optsWithGlobals();
218
+ const { verifyStatusCommand } = await import('./commands/verify-ops.js');
219
+ return runPublicCommand(() => verifyStatusCommand(run, options), options.json);
220
+ });
221
+ verify
222
+ .command('watch')
223
+ .description('Follow a run until it finishes; exit 0 all passed, 1 tests failed, 2 run/infra broke')
224
+ .argument('[run]', verifyRunArg)
225
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
226
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
227
+ .option('--interval <seconds>', 'Poll interval (default 3)')
228
+ .option('--timeout <minutes>', 'Give up after this long (default: wait forever)')
229
+ .option('--json', 'Suppress streaming; print the final summary as JSON')
230
+ .action(async (run, _options, cmd) => {
231
+ const options = cmd.optsWithGlobals();
232
+ const { verifyWatchCommand } = await import('./commands/verify-ops.js');
233
+ return runPublicCommand(() => verifyWatchCommand(run, options), options.json);
234
+ });
235
+ verify
236
+ .command('list')
237
+ .description('Runs newest first: chapter, verdict counts, age (control plane, else disk)')
238
+ .option('--limit <n>', 'Max runs to show (default 20)')
239
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
240
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
241
+ .option('--json', 'Machine-readable list')
242
+ .action(async (_options, cmd) => {
243
+ const options = cmd.optsWithGlobals();
244
+ const { verifyListCommand } = await import('./commands/verify-ops.js');
245
+ return runPublicCommand(() => verifyListCommand(options), options.json);
246
+ });
247
+ verify
248
+ .command('findings')
249
+ .description('Every problem the run found, in one screen: what failed, which readings moved, live links')
250
+ .argument('[run]', verifyRunArg)
251
+ .option('--full', 'Print the judge\'s full explanation for each finding')
252
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
253
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
254
+ .option('--json', 'Machine-readable findings')
255
+ .action(async (run, _options, cmd) => {
256
+ const options = cmd.optsWithGlobals();
257
+ const { verifyFindingsCommand } = await import('./commands/verify-ops.js');
258
+ return runPublicCommand(() => verifyFindingsCommand(run, options), options.json);
259
+ });
260
+ verify
261
+ .command('open')
262
+ .description('Open a run artifact (screenshot/video/DOM) or a failing test\'s live environment')
263
+ .argument('<run>', verifyRunArg)
264
+ .argument('<target>', 'Test selector (ordinal/title), or an artifact file name (….png/.webm/.html)')
265
+ .option('--live', 'Open the test\'s live environment URL instead of an artifact')
266
+ .option('--side <side>', 'base or head (default head)')
267
+ .option('--step <n>', 'Open the screenshot captured after replay step n')
268
+ .option('--kind <kind>', 'screenshot | video | dom (default: video, else last screenshot)')
269
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
270
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
271
+ .action(async (run, target, _options, cmd) => {
272
+ const options = cmd.optsWithGlobals();
273
+ const { verifyOpenCommand } = await import('./commands/verify-ops.js');
274
+ return runPublicCommand(() => verifyOpenCommand(run, target, options));
275
+ });
276
+ verify
277
+ .command('diff')
278
+ .description('Open a base-vs-head screenshot differ for one test (1 what changed / 2 before / 3 after)')
279
+ .argument('<run>', verifyRunArg)
280
+ .argument('<cell>', 'Test selector: ordinal, cell id, or title substring')
281
+ .option('--step <n>', 'Compare only the screenshot captured after replay step n')
282
+ .option('--out <dir>', 'Where to build the diff page (default: a temp directory)')
283
+ .option('--no-open', 'Build the page but do not open it')
284
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
285
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
286
+ .option('--json', 'Machine-readable result (implies --no-open)')
287
+ .action(async (run, cell, _options, cmd) => {
288
+ const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
289
+ const { verifyDiffCommand } = await import('./commands/verify-ops.js');
290
+ return runPublicCommand(() => verifyDiffCommand(run, cell, options), options.json);
291
+ });
292
+ verify
293
+ .command('cleanup')
294
+ .description('Show exact retained-state references, provider deletion, and cleanup-SLO status')
295
+ .argument('<run>', verifyRunArg)
296
+ .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
297
+ .option('--side <side>', 'base or head (default head)')
298
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
299
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
300
+ .option('--json', 'Machine-readable cleanup status')
301
+ .action(async (run, cell, _options, cmd) => {
302
+ const options = cmd.optsWithGlobals();
303
+ const { verifyCleanupCommand } = await import('./commands/verify-ops.js');
304
+ return runPublicCommand(() => verifyCleanupCommand(run, cell, options), options.json);
305
+ });
306
+ verify
307
+ .command('reopen')
308
+ .description('Attach fresh E2B compute to retained Archil state from a test or exact fork ID')
309
+ .argument('<source>', 'Run selector, or an exact archil:…:branch:… fork ID')
310
+ .argument('[cell]', 'Stable test selector for a run: ordinal, riskCellId, or exact planner cellKey')
311
+ .option('--side <side>', 'base or head (default head)')
312
+ .option('--open-browser', 'Open the restarted application in a browser')
313
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
314
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
315
+ .option('--json', 'Machine-readable attachment details')
316
+ .action(async (source, cell, _options, cmd) => {
317
+ const options = cmd.optsWithGlobals();
318
+ const { verifyReopenCommand } = await import('./commands/verify-ops.js');
319
+ return runPublicCommand(() => verifyReopenCommand(source, cell, options), options.json);
320
+ });
321
+ verify
322
+ .command('refork')
323
+ .description('Checkpoint a reopened Archil universe into a durable child fork')
324
+ .argument('<source>', 'Run selector, or an exact archil:…:branch:… parent fork ID')
325
+ .argument('<cell-or-child-key>', 'Stable test selector for a run, or child key for an exact fork ID')
326
+ .argument('[child-key]', 'Structured stable child key when source is a run')
327
+ .option('--side <side>', 'base or head (default head; only applies to a run)')
328
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
329
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
330
+ .option('--json', 'Machine-readable parent, child, checkpoint, and expiry')
331
+ .action(async (source, cellOrChildKey, childKey, _options, cmd) => {
332
+ const options = cmd.optsWithGlobals();
333
+ const { verifyReforkCommand } = await import('./commands/verify-ops.js');
334
+ return runPublicCommand(() => verifyReforkCommand(source, cellOrChildKey, childKey, options), options.json);
335
+ });
336
+ verify
337
+ .command('shell')
338
+ .description('Terminal into a failing test\'s live universe (e2b sandbox connect)')
339
+ .argument('<run>', verifyRunArg)
340
+ .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
341
+ .option('--side <side>', 'base or head (default head; only failing tests keep a universe alive)')
342
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
343
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
344
+ .action(async (run, cell, _options, cmd) => {
345
+ const options = cmd.optsWithGlobals();
346
+ const { verifyShellCommand } = await import('./commands/verify-ops.js');
347
+ return runPublicCommand(() => verifyShellCommand(run, cell, options));
348
+ });
349
+ verify
350
+ .command('show')
351
+ .description('One test in full: finding, changed readings, request timeline, SDK log lines')
352
+ .argument('<run>', verifyRunArg)
353
+ .argument('<cell>', 'Test ordinal, cell id, planner cell key, or title substring')
354
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
355
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
356
+ .option('--no-timeline', 'Skip the request timeline')
357
+ .option('--json', 'Machine-readable detail')
358
+ .action(async (run, cell, _options, cmd) => {
359
+ const options = cmd.optsWithGlobals();
360
+ const { verifyShowCommand } = await import('./commands/verify-ops.js');
361
+ return runPublicCommand(() => verifyShowCommand(run, cell, options), options.json);
362
+ });
363
+ verify
364
+ .command('artifacts')
365
+ .description('Pull a run\'s screenshots, DOM snapshots and videos to a local directory')
366
+ .argument('<run>', verifyRunArg)
367
+ .option('--out <dir>', 'Destination (default: ./verify-artifacts/<runId>)')
368
+ .option('--cell <selector>', 'Only artifacts for one test (ordinal, cell id, or title substring)')
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 result')
372
+ .action(async (run, _options, cmd) => {
373
+ const options = cmd.optsWithGlobals();
374
+ const { verifyArtifactsCommand } = await import('./commands/verify-ops.js');
375
+ return runPublicCommand(() => verifyArtifactsCommand(run, options), options.json);
376
+ });
377
+ verify
378
+ .command('sandboxes')
379
+ .description('E2B sandbox inventory with run attribution; --kill-stale reclaims superseded ones')
380
+ .option('--kill-stale', 'Kill verifier-attributed sandboxes superseded by newer runs (never unattributed ones)')
381
+ .option('--kill-run <runId>', 'Kill every sandbox attributed to one run')
382
+ .option('--server <url>', 'Control-plane URL, preferred over direct E2B access (default: http://127.0.0.1:3000)')
383
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
384
+ .option('--json', 'Machine-readable inventory')
385
+ .action(async (_options, cmd) => {
386
+ const options = cmd.optsWithGlobals();
387
+ const { verifySandboxesCommand } = await import('./commands/verify-sandboxes.js');
388
+ return runPublicCommand(() => verifySandboxesCommand(options), options.json);
389
+ });
390
+ verify
391
+ .command('reseal')
392
+ .description('Rebuild a sealed specimen bundle (guarded) and restart the control plane onto it')
393
+ .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
394
+ .option('--source <dir>', 'Specimen source directory holding reseal.sh')
395
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
396
+ .option('--repo-root <path>', 'Repository holding demo/cloud-verifier (default: walk up from cwd)')
397
+ .option('--no-restart', 'Only rebuild the bundle; leave the running server on the old checkout')
398
+ .action(async (specimen, _options, cmd) => {
399
+ const options = cmd.optsWithGlobals();
400
+ const { verifyResealCommand } = await import('./commands/verify-reseal.js');
401
+ return runPublicCommand(() => verifyResealCommand(specimen, options));
402
+ });
403
+ verify
404
+ .command('mcp')
405
+ .description('Run a stdio MCP server exposing the verifier to agents (verify_start/verify_wait/…)')
406
+ .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
407
+ .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
408
+ .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
409
+ .action(async (_options, cmd) => {
410
+ const options = cmd.optsWithGlobals();
411
+ try {
412
+ // Lazy import, same reasoning as the top-level `mcp` command: only this
413
+ // path needs @modelcontextprotocol/sdk.
414
+ const { runVerifyMcpServer } = await import('./commands/verify-mcp.js');
415
+ await runVerifyMcpServer(options);
416
+ }
417
+ catch (err) {
418
+ console.error(chalk.red('verify mcp server failed:'), err instanceof Error ? err.message : err);
419
+ process.exit(1);
420
+ }
421
+ });
158
422
  program
159
423
  .command('login')
160
424
  .description('Add or refresh a saved GitHub account (use --headless for CI tokens)')
@@ -247,7 +511,9 @@ program
247
511
  .option('--force', 'Skip pre-PR triage checks')
248
512
  .addOption(new Option('--auto-fix', 'Alpha auto-fix for entitled repositories').hideHelp())
249
513
  .option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
514
+ .option('--no-auto-merge', 'Do not apply the auto-merge label')
250
515
  .option('--no-wait', 'Skip waiting for analysis results')
516
+ .option('--json', 'Machine-readable: one JSON document on stdout (see `haystack schema submit`), progress on stderr')
251
517
  .option('--max-turns <n>', 'Max agentic turns per triage checker (overrides .haystack.json triage.maxTurns)', (v) => parseInt(v, 10))
252
518
  .option('--triage-timeout <seconds>', 'Wall-clock timeout per triage checker in seconds (overrides .haystack.json triage.timeoutMs)', (v) => parseInt(v, 10))
253
519
  .addHelpText('after', `
@@ -296,8 +562,22 @@ Review Routing:
296
562
  needs-assignment queue (a teammate will pick it up)
297
563
  • --review <username> Same label, plus requests review from that GitHub user
298
564
 
565
+ Machine-readable output (--json):
566
+ stdout carries exactly one JSON document (schema: \`haystack schema submit\`);
567
+ all progress goes to stderr. On success the payload includes the PR ref,
568
+ the resolved title/body and their sources, auto-merge/auto-fix state with
569
+ source, and the analysis outcome. On failure it is an error envelope
570
+ { status: "error", error: "..." }.
571
+
572
+ Exit codes:
573
+ 0 PR created/updated (including verdict "needs_input" — the PR exists;
574
+ read the verdict from the JSON or from \`haystack triage\`)
575
+ 1 Hard failure (bad flags, auth, push, PR creation, blocking triage
576
+ findings, or the analysis status check itself failing)
577
+
299
578
  Examples:
300
579
  haystack submit # Recommended: triage, create PR, auto-merge queue
580
+ haystack submit --json # Agent mode: JSON on stdout, progress on stderr
301
581
  haystack submit --force # Skip triage
302
582
  haystack submit --no-wait # Don't wait for analysis
303
583
  haystack submit --title "Fix auth" # Custom PR title
@@ -312,17 +592,23 @@ Examples:
312
592
  haystack submit --max-turns 12 # Raise triage turn cap to 12 per checker
313
593
  haystack submit --triage-timeout 300 # Raise triage wall-clock to 5 minutes
314
594
  `)
315
- .action(async (options) => {
316
- // Resolve --auto-merge default from .haystack.json when not explicitly set
317
- if (options.autoMerge === undefined) {
595
+ .action(async (options, command) => {
596
+ // Resolve --auto-merge / --auto-fix defaults from .haystack.json when not
597
+ // explicitly set, and record WHERE each value came from so submit can
598
+ // announce the source (silent config-driven defaults are invisible to
599
+ // the caller otherwise).
600
+ const autoMergeFromCli = command.getOptionValueSource('autoMerge') === 'cli';
601
+ const autoFixFromCli = command.getOptionValueSource('autoFix') === 'cli';
602
+ if (!autoMergeFromCli) {
318
603
  options.autoMerge = await isAutoMergeEnabled();
319
604
  }
320
- // Resolve --auto-fix default from .haystack.json (preferences.auto_fix).
321
605
  // Auto-fix remains alpha — the explicit --auto-fix flag still surfaces
322
606
  // discouragement warnings; opting in via repo config is the supported path.
323
- if (options.autoFix === undefined) {
607
+ if (!autoFixFromCli) {
324
608
  options.autoFix = await isAutoFixEnabled();
325
609
  }
610
+ options.autoMergeSource = autoMergeFromCli ? 'flag' : 'config';
611
+ options.autoFixSource = autoFixFromCli ? 'flag' : 'config';
326
612
  return submitCommand(options);
327
613
  });
328
614
  program
@@ -364,35 +650,36 @@ inbox
364
650
  .command('list')
365
651
  .description('List PRs in your Haystack inbox')
366
652
  .option('--json', 'Output as JSON')
367
- .action((options) => runPublicCommand(() => inboxListCommand(options)));
653
+ .action((options) => runPublicCommand(() => inboxListCommand(options), options.json));
368
654
  const prProgram = program.command('pr').description('Inspect one pull request');
369
655
  prProgram
370
656
  .command('get <ref>')
371
657
  .description('Get triage, merge blockers, and trace availability')
372
658
  .option('--json', 'Output as JSON')
373
- .action((ref, options) => runPublicCommand(() => prReadCommand(ref, options)));
659
+ .action((ref, options) => runPublicCommand(() => prReadCommand(ref, options), options.json));
374
660
  program
375
661
  .command('ask <ref> <question>')
376
662
  .description('Ask Haystack about a pull request')
377
663
  .option('--json', 'Output the answer and every consulted customer-facing source as JSON')
378
664
  .option('--session <id>', 'Continue a previous Ask Haystack session')
379
- .action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options)));
665
+ .action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options), options.json));
380
666
  const traces = program.command('traces').description('Inspect customer-owned Entire traces');
381
667
  traces
382
668
  .command('list <ref>')
383
669
  .description('List retained checkpoints for a pull request')
384
670
  .option('--json', 'Output as JSON')
385
- .action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options)));
671
+ .action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options), options.json));
386
672
  traces
387
673
  .command('get <ref> <checkpoint>')
388
674
  .description('Get retained transcript chunks for one checkpoint')
389
675
  .option('--cursor <cursor>', 'Zero-based chunk cursor')
390
676
  .option('--limit <count>', 'Chunks per page', '20')
391
677
  .option('--json', 'Output as JSON')
392
- .action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options)));
678
+ .action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options), options.json));
393
679
  program
394
680
  .command('dismiss <pr>')
395
681
  .description('Dismiss analysis findings for a PR')
682
+ .option('--json', 'Machine-readable output (see `haystack schema action`)')
396
683
  .addHelpText('after', `
397
684
  Dismiss analysis findings for a PR, moving it from "Issues Found" to
398
685
  "Good to Merge" in the Haystack feed.
@@ -414,6 +701,7 @@ Examples:
414
701
  program
415
702
  .command('mark-reviewed <pr>')
416
703
  .description('Mark human review as not needed for a PR')
704
+ .option('--json', 'Machine-readable output (see `haystack schema action`)')
417
705
  .addHelpText('after', `
418
706
  Mark human review as not needed for a PR, moving it from "Needs Review"
419
707
  to "Good to Merge" in the Haystack feed.
@@ -435,6 +723,7 @@ Examples:
435
723
  program
436
724
  .command('undismiss <pr>')
437
725
  .description('Undo a dismiss or mark-reviewed override for a PR')
726
+ .option('--json', 'Machine-readable output (see `haystack schema action`)')
438
727
  .addHelpText('after', `
439
728
  Clear all overrides (dismissed findings and/or review-not-needed) for a PR,
440
729
  returning it to its original feed bucket.
@@ -452,8 +741,9 @@ Examples:
452
741
  .action(undismissCommand);
453
742
  program
454
743
  .command('review [pr]')
455
- .description('Trigger a fresh Haystack analysis for a PR')
744
+ .description('Trigger a fresh Haystack analysis for a PR (machine analysis — for HUMAN review use request-review)')
456
745
  .option('--no-wait', 'Exit after triggering instead of waiting for results')
746
+ .option('--json', 'Machine-readable output (see `haystack schema action`)')
457
747
  .addHelpText('after', `
458
748
  Re-run full Haystack analysis on a PR's current head.
459
749
 
@@ -481,6 +771,7 @@ Examples:
481
771
  program
482
772
  .command('request-review <pr> [reviewer]')
483
773
  .description('Tag a PR as needing human review')
774
+ .option('--json', 'Machine-readable output (see `haystack schema action`)')
484
775
  .addHelpText('after', `
485
776
  Tag any PR with "needs human review", even after it was created.
486
777
  Adds the haystack:needs-review label to the PR, moving it from
@@ -551,9 +842,9 @@ config
551
842
  .description('Auto-merge safe PRs submitted via haystack submit (on|off|status)')
552
843
  .addHelpText('after', `
553
844
  Actions:
554
- on, enable Enable auto-merge for safe PRs
555
- off, disable Disable auto-merge
556
- status Show current status (default)
845
+ on, enable, true Enable auto-merge for safe PRs
846
+ off, disable, false Disable auto-merge
847
+ status Show current status (default)
557
848
 
558
849
  When enabled, PRs submitted via \`haystack submit\` will be
559
850
  automatically merged if Haystack analysis finds no issues
@@ -571,9 +862,9 @@ config
571
862
  .description('(Alpha) Auto-fix mechanical issues on PRs submitted via haystack submit (on|off|status)')
572
863
  .addHelpText('after', `
573
864
  Actions:
574
- on, enable Enable auto-fix for mechanical issues
575
- off, disable Disable auto-fix
576
- status Show current status (default)
865
+ on, enable, true Enable auto-fix for mechanical issues
866
+ off, disable, false Disable auto-fix
867
+ status Show current status (default)
577
868
 
578
869
  ⚠ Auto-fix is alpha. When enabled, PRs submitted via \`haystack submit\`
579
870
  are labeled haystack:auto-fix. The Haystack analysis pipeline then
@@ -882,6 +1173,38 @@ rules
882
1173
  process.exit(1);
883
1174
  }
884
1175
  });
1176
+ // ─── system-map ──────────────────────────────────────────────────────────────
1177
+ const systemMap = program
1178
+ .command('system-map')
1179
+ .description('Manage .haystack/system.yml — runtime facts that help Haystack QA run your system');
1180
+ systemMap
1181
+ .command('validate')
1182
+ .description('Check .haystack/system.yml against the rules Haystack QA will apply')
1183
+ .option('--json', 'Machine-readable result')
1184
+ .addHelpText('after', `
1185
+ The system map holds VERIFIED facts about how your system works — install/
1186
+ build commands, how services boot and signal readiness, seed/reset commands,
1187
+ dev-only test logins, external services that must never be touched live, and
1188
+ prebuilt CI artifacts. Haystack QA uses these facts to run more of what it
1189
+ already wants to test; the file never selects tests.
1190
+
1191
+ Author it with the /map-your-system skill (haystack skills install), then run
1192
+ this before committing. Checks: valid YAML + version, no test-selection keys,
1193
+ no secret values, non-empty boot/login/run commands, artifact declarations.
1194
+
1195
+ Examples:
1196
+ haystack system-map validate
1197
+ haystack system-map validate --json
1198
+ `)
1199
+ .action(async (opts) => {
1200
+ try {
1201
+ await systemMapValidateCommand(opts);
1202
+ }
1203
+ catch (err) {
1204
+ console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
1205
+ process.exit(1);
1206
+ }
1207
+ });
885
1208
  // ─── mcp ─────────────────────────────────────────────────────────────────────
886
1209
  program
887
1210
  .command('mcp')
package/dist/schema.d.ts CHANGED
@@ -13,9 +13,13 @@ export declare const SCHEMA_VERSIONS: {
13
13
  readonly triage: "2.0.0";
14
14
  readonly setup: "1.0.0";
15
15
  readonly pr: "2.0.0";
16
+ readonly 'pr-status': "1.0.0";
16
17
  readonly inbox: "1.0.0";
17
18
  readonly ask: "1.0.0";
18
19
  readonly traces: "1.0.0";
20
+ readonly submit: "1.0.0";
21
+ readonly action: "1.0.0";
22
+ readonly error: "1.0.0";
19
23
  };
20
24
  export type SchemaName = keyof typeof SCHEMA_VERSIONS;
21
25
  /** Wrap a payload with the `schema_version` envelope. */
package/dist/schema.js CHANGED
@@ -13,9 +13,13 @@ export const SCHEMA_VERSIONS = {
13
13
  triage: '2.0.0',
14
14
  setup: '1.0.0',
15
15
  pr: '2.0.0',
16
+ 'pr-status': '1.0.0',
16
17
  inbox: '1.0.0',
17
18
  ask: '1.0.0',
18
19
  traces: '1.0.0',
20
+ submit: '1.0.0',
21
+ action: '1.0.0',
22
+ error: '1.0.0',
19
23
  };
20
24
  /** Wrap a payload with the `schema_version` envelope. */
21
25
  export function withSchema(name, payload) {