@haystackeditor/cli 0.15.30 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +111 -60
  2. package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
  3. package/dist/assets/hooks/scripts/post-commit.sh +3 -0
  4. package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
  5. package/dist/assets/hooks/scripts/pre-push.sh +3 -0
  6. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  7. package/dist/commands/case-batch-contract.js +608 -0
  8. package/dist/commands/case-batch.js +713 -0
  9. package/dist/commands/cloud-verifier-identity-census.js +5 -2
  10. package/dist/commands/combination-search-hook.js +139 -0
  11. package/dist/commands/design-verify.js +27 -1
  12. package/dist/commands/dismiss.js +2 -1
  13. package/dist/commands/hooks.js +66 -7
  14. package/dist/commands/install-session-hooks.js +131 -44
  15. package/dist/commands/policy.js +32 -45
  16. package/dist/commands/precompute-delivery.js +6 -1
  17. package/dist/commands/scaffold-provisional-universe.js +8 -10
  18. package/dist/commands/setup.js +32 -10
  19. package/dist/commands/submit.js +7 -2
  20. package/dist/commands/telemetry.js +17 -2
  21. package/dist/commands/triage.js +2 -1
  22. package/dist/commands/verify-explore.js +1 -0
  23. package/dist/commands/verify-history.js +152 -0
  24. package/dist/commands/verify-hosted-mcp.js +3 -12
  25. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  26. package/dist/commands/verify-hosted.js +180 -163
  27. package/dist/commands/verify-precompute.js +57 -10
  28. package/dist/commands/verify.js +51 -139
  29. package/dist/index.js +264 -348
  30. package/dist/lazy.js +8 -0
  31. package/dist/schema.js +1 -0
  32. package/dist/tools/detect.js +3 -24
  33. package/dist/triage/prompts.js +46 -45
  34. package/dist/triage/runner.js +119 -33
  35. package/dist/types/verify-history.js +1 -0
  36. package/dist/types.js +3 -3
  37. package/dist/utils/auth.js +14 -2
  38. package/dist/utils/design-verifier-api.js +23 -0
  39. package/dist/utils/design-verifier-history.js +79 -0
  40. package/dist/utils/git.js +60 -29
  41. package/dist/utils/github-api.js +14 -1
  42. package/dist/utils/haystack-api.js +26 -7
  43. package/dist/utils/hooks.js +43 -6
  44. package/dist/utils/prompter.js +25 -10
  45. package/dist/utils/safe-write.js +31 -0
  46. package/dist/utils/secret-paths.js +115 -0
  47. package/dist/utils/secrets.js +0 -1
  48. package/dist/utils/telemetry.js +34 -14
  49. package/dist/utils/update-check.js +151 -0
  50. package/package.json +18 -14
  51. package/schemas/case-batch.v1.json +184 -0
  52. package/schemas/cloud-verifier.v1.json +55 -100
  53. package/dist/commands/ask.d.ts +0 -14
  54. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  55. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  56. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  57. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  58. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  59. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  60. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  61. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  62. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  63. package/dist/commands/config.d.ts +0 -46
  64. package/dist/commands/design-verify.d.ts +0 -31
  65. package/dist/commands/dismiss.d.ts +0 -29
  66. package/dist/commands/hooks.d.ts +0 -13
  67. package/dist/commands/inbox.d.ts +0 -65
  68. package/dist/commands/init.d.ts +0 -10
  69. package/dist/commands/install-session-hooks.d.ts +0 -17
  70. package/dist/commands/login.d.ts +0 -8
  71. package/dist/commands/mcp.d.ts +0 -1
  72. package/dist/commands/policy.d.ts +0 -31
  73. package/dist/commands/pr-status.d.ts +0 -144
  74. package/dist/commands/pr.d.ts +0 -40
  75. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  76. package/dist/commands/precompute-delivery.d.ts +0 -20
  77. package/dist/commands/prepare-universe-review.d.ts +0 -115
  78. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  79. package/dist/commands/request-review.d.ts +0 -26
  80. package/dist/commands/review.d.ts +0 -25
  81. package/dist/commands/rules.d.ts +0 -4
  82. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  83. package/dist/commands/schema-cmd.d.ts +0 -2
  84. package/dist/commands/setup.d.ts +0 -28
  85. package/dist/commands/skills.d.ts +0 -8
  86. package/dist/commands/status.d.ts +0 -4
  87. package/dist/commands/submit.d.ts +0 -30
  88. package/dist/commands/system-map.d.ts +0 -42
  89. package/dist/commands/telemetry.d.ts +0 -53
  90. package/dist/commands/tokens.d.ts +0 -14
  91. package/dist/commands/triage.d.ts +0 -35
  92. package/dist/commands/verify-core.d.ts +0 -449
  93. package/dist/commands/verify-core.js +0 -789
  94. package/dist/commands/verify-explore.d.ts +0 -14
  95. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  96. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  97. package/dist/commands/verify-hosted.d.ts +0 -92
  98. package/dist/commands/verify-mcp.d.ts +0 -8
  99. package/dist/commands/verify-mcp.js +0 -517
  100. package/dist/commands/verify-ops.d.ts +0 -158
  101. package/dist/commands/verify-ops.js +0 -1148
  102. package/dist/commands/verify-precompute.d.ts +0 -30
  103. package/dist/commands/verify-reproducibility.d.ts +0 -85
  104. package/dist/commands/verify-reproducibility.js +0 -494
  105. package/dist/commands/verify-reseal.d.ts +0 -9
  106. package/dist/commands/verify-reseal.js +0 -148
  107. package/dist/commands/verify-sandboxes.d.ts +0 -95
  108. package/dist/commands/verify-sandboxes.js +0 -352
  109. package/dist/commands/verify.d.ts +0 -28
  110. package/dist/commands/webhooks.d.ts +0 -30
  111. package/dist/index.d.ts +0 -22
  112. package/dist/schema.d.ts +0 -28
  113. package/dist/states.d.ts +0 -29
  114. package/dist/tools/detect.d.ts +0 -50
  115. package/dist/triage/prompts.d.ts +0 -24
  116. package/dist/triage/runner.d.ts +0 -34
  117. package/dist/triage/types.d.ts +0 -42
  118. package/dist/types.d.ts +0 -1684
  119. package/dist/utils/action-output.d.ts +0 -24
  120. package/dist/utils/analysis-api.d.ts +0 -187
  121. package/dist/utils/auth.d.ts +0 -79
  122. package/dist/utils/config.d.ts +0 -24
  123. package/dist/utils/design-verifier-api.d.ts +0 -200
  124. package/dist/utils/design-verifier-result.d.ts +0 -74
  125. package/dist/utils/detect.d.ts +0 -43
  126. package/dist/utils/git.d.ts +0 -135
  127. package/dist/utils/github-api.d.ts +0 -104
  128. package/dist/utils/haystack-api.d.ts +0 -37
  129. package/dist/utils/hooks.d.ts +0 -12
  130. package/dist/utils/pending-state.d.ts +0 -40
  131. package/dist/utils/pr-ref.d.ts +0 -27
  132. package/dist/utils/prompter.d.ts +0 -85
  133. package/dist/utils/secrets.d.ts +0 -47
  134. package/dist/utils/telemetry.d.ts +0 -19
  135. /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
package/dist/index.js CHANGED
@@ -24,39 +24,71 @@ import { fileURLToPath } from 'node:url';
24
24
  import { dirname, join } from 'node:path';
25
25
  import chalk from 'chalk';
26
26
  import { Command, Option } from 'commander';
27
- import { statusCommand } from './commands/status.js';
28
27
  import { withSchema } from './schema.js';
29
- import { initCommand } from './commands/init.js';
30
- import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
31
- import { handleAgenticTool, handleAutoMerge, handleAutoFix, handleWaitForReviewers, isAutoMergeEnabled, isAutoFixEnabled } from './commands/config.js';
32
- import { installSkills, listSkills } from './commands/skills.js';
33
- import { cloudVerifierUniverseValidateCommand } from './commands/cloud-verifier-universe.js';
34
- import { cloudVerifierIdentityCensusCommand } from './commands/cloud-verifier-identity-census.js';
35
- import { cloudVerifierDataStoreDriftCommand } from './commands/cloud-verifier-data-store-drift.js';
36
- import { cloudVerifierBehaviorsValidateCommand } from './commands/cloud-verifier-behaviors.js';
37
- import { prepareUniverseReviewCommand } from './commands/prepare-universe-review.js';
38
- import { scaffoldProvisionalUniverseCommand } from './commands/scaffold-provisional-universe.js';
39
- import { hooksInstall, hooksStatus } from './commands/hooks.js';
40
- import { submitCommand } from './commands/submit.js';
41
- import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
42
- import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
43
- import { triageCommand } from './commands/triage.js';
44
- import { designVerifyCommand } from './commands/design-verify.js';
45
- import { dismissCommand, markReviewedCommand, undismissCommand } from './commands/dismiss.js';
46
- import { requestReviewCommand } from './commands/request-review.js';
47
- import { reviewCommand } from './commands/review.js';
48
- import { prStatusCommand } from './commands/pr-status.js';
49
- import { prReadCommand } from './commands/pr.js';
50
- import { inboxListCommand } from './commands/inbox.js';
51
- import { askHaystackCommand } from './commands/ask.js';
52
- import { telemetryInstrumentCommand } from './commands/telemetry.js';
53
- import { setupCommand } from './commands/setup.js';
54
- import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
55
- import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
56
- import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
57
- import { systemMapValidateCommand } from './commands/system-map.js';
58
- import { verifyCommand } from './commands/verify.js';
59
- import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
28
+ import { lazy } from './lazy.js';
29
+ // Command modules load on first use (see src/lazy.ts). Keep new commands
30
+ // on this pattern: a static import here puts its whole dependency graph on
31
+ // the startup path of every invocation, including --version and --help.
32
+ const statusCommand = lazy(() => import('./commands/status.js'), 'statusCommand');
33
+ const initCommand = lazy(() => import('./commands/init.js'), 'initCommand');
34
+ const authListCommand = lazy(() => import('./commands/login.js'), 'authListCommand');
35
+ const authUseCommand = lazy(() => import('./commands/login.js'), 'authUseCommand');
36
+ const loginCommand = lazy(() => import('./commands/login.js'), 'loginCommand');
37
+ const logoutCommand = lazy(() => import('./commands/login.js'), 'logoutCommand');
38
+ const whoamiCommand = lazy(() => import('./commands/login.js'), 'whoamiCommand');
39
+ const handleAgenticTool = lazy(() => import('./commands/config.js'), 'handleAgenticTool');
40
+ const handleAutoMerge = lazy(() => import('./commands/config.js'), 'handleAutoMerge');
41
+ const handleAutoFix = lazy(() => import('./commands/config.js'), 'handleAutoFix');
42
+ const handleWaitForReviewers = lazy(() => import('./commands/config.js'), 'handleWaitForReviewers');
43
+ const isAutoMergeEnabled = lazy(() => import('./commands/config.js'), 'isAutoMergeEnabled');
44
+ const isAutoFixEnabled = lazy(() => import('./commands/config.js'), 'isAutoFixEnabled');
45
+ const installSkills = lazy(() => import('./commands/skills.js'), 'installSkills');
46
+ const listSkills = lazy(() => import('./commands/skills.js'), 'listSkills');
47
+ const cloudVerifierUniverseValidateCommand = lazy(() => import('./commands/cloud-verifier-universe.js'), 'cloudVerifierUniverseValidateCommand');
48
+ const cloudVerifierIdentityCensusCommand = lazy(() => import('./commands/cloud-verifier-identity-census.js'), 'cloudVerifierIdentityCensusCommand');
49
+ const cloudVerifierDataStoreDriftCommand = lazy(() => import('./commands/cloud-verifier-data-store-drift.js'), 'cloudVerifierDataStoreDriftCommand');
50
+ const cloudVerifierBehaviorsValidateCommand = lazy(() => import('./commands/cloud-verifier-behaviors.js'), 'cloudVerifierBehaviorsValidateCommand');
51
+ const prepareUniverseReviewCommand = lazy(() => import('./commands/prepare-universe-review.js'), 'prepareUniverseReviewCommand');
52
+ const scaffoldProvisionalUniverseCommand = lazy(() => import('./commands/scaffold-provisional-universe.js'), 'scaffoldProvisionalUniverseCommand');
53
+ const hooksInstall = lazy(() => import('./commands/hooks.js'), 'hooksInstall');
54
+ const hooksStatus = lazy(() => import('./commands/hooks.js'), 'hooksStatus');
55
+ const submitCommand = lazy(() => import('./commands/submit.js'), 'submitCommand');
56
+ const installSessionHooks = lazy(() => import('./commands/install-session-hooks.js'), 'installSessionHooks');
57
+ const sessionHooksStatus = lazy(() => import('./commands/install-session-hooks.js'), 'sessionHooksStatus');
58
+ const listPolicies = lazy(() => import('./commands/policy.js'), 'listPolicies');
59
+ const addPolicy = lazy(() => import('./commands/policy.js'), 'addPolicy');
60
+ const removePolicy = lazy(() => import('./commands/policy.js'), 'removePolicy');
61
+ const initPolicies = lazy(() => import('./commands/policy.js'), 'initPolicies');
62
+ const addInstruction = lazy(() => import('./commands/policy.js'), 'addInstruction');
63
+ const triageCommand = lazy(() => import('./commands/triage.js'), 'triageCommand');
64
+ const designVerifyCommand = lazy(() => import('./commands/design-verify.js'), 'designVerifyCommand');
65
+ const dismissCommand = lazy(() => import('./commands/dismiss.js'), 'dismissCommand');
66
+ const markReviewedCommand = lazy(() => import('./commands/dismiss.js'), 'markReviewedCommand');
67
+ const undismissCommand = lazy(() => import('./commands/dismiss.js'), 'undismissCommand');
68
+ const requestReviewCommand = lazy(() => import('./commands/request-review.js'), 'requestReviewCommand');
69
+ const reviewCommand = lazy(() => import('./commands/review.js'), 'reviewCommand');
70
+ const prStatusCommand = lazy(() => import('./commands/pr-status.js'), 'prStatusCommand');
71
+ const prReadCommand = lazy(() => import('./commands/pr.js'), 'prReadCommand');
72
+ const inboxListCommand = lazy(() => import('./commands/inbox.js'), 'inboxListCommand');
73
+ const askHaystackCommand = lazy(() => import('./commands/ask.js'), 'askHaystackCommand');
74
+ const telemetryInstrumentCommand = lazy(() => import('./commands/telemetry.js'), 'telemetryInstrumentCommand');
75
+ const setupCommand = lazy(() => import('./commands/setup.js'), 'setupCommand');
76
+ const schemaCommand = lazy(() => import('./commands/schema-cmd.js'), 'schemaCommand');
77
+ const listSchemas = lazy(() => import('./commands/schema-cmd.js'), 'listSchemas');
78
+ const registerWebhook = lazy(() => import('./commands/webhooks.js'), 'registerWebhook');
79
+ const listWebhooks = lazy(() => import('./commands/webhooks.js'), 'listWebhooks');
80
+ const rotateWebhookSecret = lazy(() => import('./commands/webhooks.js'), 'rotateWebhookSecret');
81
+ const setWebhookEnabled = lazy(() => import('./commands/webhooks.js'), 'setWebhookEnabled');
82
+ const listDeliveries = lazy(() => import('./commands/webhooks.js'), 'listDeliveries');
83
+ const replayDelivery = lazy(() => import('./commands/webhooks.js'), 'replayDelivery');
84
+ const editRulesCommand = lazy(() => import('./commands/rules.js'), 'editRulesCommand');
85
+ const validateRulesCommand = lazy(() => import('./commands/rules.js'), 'validateRulesCommand');
86
+ const systemMapValidateCommand = lazy(() => import('./commands/system-map.js'), 'systemMapValidateCommand');
87
+ const verifyCommand = lazy(() => import('./commands/verify.js'), 'verifyCommand');
88
+ const headlessLoginCommand = lazy(() => import('./commands/tokens.js'), 'headlessLoginCommand');
89
+ const headlessLogoutCommand = lazy(() => import('./commands/tokens.js'), 'headlessLogoutCommand');
90
+ const listTokensCommand = lazy(() => import('./commands/tokens.js'), 'listTokensCommand');
91
+ const revokeTokenCommand = lazy(() => import('./commands/tokens.js'), 'revokeTokenCommand');
60
92
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
61
93
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
62
94
  // the startup import graph means a missing/broken SDK can never crash core commands
@@ -99,17 +131,14 @@ const program = new Command();
99
131
  program
100
132
  .name('haystack')
101
133
  .description('Haystack CLI — automated PR review, triage, and merge queue')
102
- .version(getVersion());
134
+ .version(getVersion())
135
+ // commander 13 made surplus positional arguments an error; the CLI has always
136
+ // ignored them. Every command created below inherits this setting.
137
+ .allowExcessArguments();
103
138
  program
104
139
  .command('schema [name]')
105
140
  .description('Print the JSON Schema for a command\'s --json output')
106
- .action((name) => {
107
- if (!name) {
108
- listSchemas();
109
- return;
110
- }
111
- schemaCommand(name);
112
- });
141
+ .action((name) => (name ? schemaCommand(name) : listSchemas()));
113
142
  program
114
143
  .command('init')
115
144
  .description('Create .haystack.json configuration')
@@ -130,9 +159,9 @@ program
130
159
  .option('--json', 'Agent mode: emit NDJSON events on stdout, read answers on stdin (no TTY prompts)')
131
160
  .option('--repo <repo...>', 'Repo(s) to configure (owner/name); skips the repo picker')
132
161
  .option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write, ENABLE auto-merge — pass --no-auto-merge to opt out)')
133
- // Defining --no-auto-merge also accepts --auto-merge; default is "ask" unless
134
- // one is explicitly passed (detected via getOptionValueSource below).
135
- .option('--no-auto-merge', 'Set auto-merge in the written config (--auto-merge / --no-auto-merge; omit to be asked)')
162
+ // Only the negative form exists; there is no --auto-merge. Without the flag the
163
+ // wizard asks (detected via getOptionValueSource below).
164
+ .option('--no-auto-merge', 'Turn auto-merge off in the written config (omit to be asked)')
136
165
  .option('--answers <file>', 'JSON file of {questionId: value} pre-supplied answers')
137
166
  .addHelpText('after', `
138
167
  Steps: verify GitHub App → select repos → scan (rules/policies/instructions) →
@@ -154,9 +183,9 @@ Examples:
154
183
  haystack setup --json --repo owner/name
155
184
  `)
156
185
  .action((options, command) => {
157
- // Tri-state auto-merge: undefined (ask) unless --auto-merge/--no-auto-merge
158
- // was explicitly passed. commander defaults autoMerge to true because of the
159
- // --no- form, so check the value source rather than the value.
186
+ // auto-merge: false when --no-auto-merge was passed, otherwise undefined (ask).
187
+ // commander defaults autoMerge to true because of the --no- form, so check
188
+ // the value source rather than the value.
160
189
  const autoMergeExplicit = command.getOptionValueSource('autoMerge') === 'cli';
161
190
  return setupCommand({
162
191
  json: options.json,
@@ -172,54 +201,73 @@ program
172
201
  .action(statusCommand);
173
202
  const verify = program
174
203
  .command('verify')
175
- .description('Verify a change in matched cloud universes, or bisect a historical regression')
176
- .argument('[specimen]', 'Launch specimen to run (default: all)')
177
- .option('--repo <owner/repo>', 'GitHub repository to verify through the authenticated hosted service')
178
- .option('--base <ref>', 'Base revision for a repository run')
179
- .option('--head <ref>', 'Head revision for a repository run')
204
+ .description('Verify the current change with the hosted Haystack fleet')
205
+ .option('--repo <owner/repo>', 'GitHub repository (default: origin remote)')
206
+ .option('--base <ref>', 'Base revision (default: merge base of HEAD and origin/default branch)')
207
+ .option('--head <ref>', 'Head revision (default: current HEAD commit)')
180
208
  .option('--intent-file <path>', 'Optional intent JSON with problem, goal, and intended_outcomes')
181
209
  .option('--idempotency-key <key>', 'Stable retry key (default: derived from repository, refs, and intent)')
182
210
  .option('--account <login>', 'Use a specific saved Haystack account')
183
- .addOption(new Option('--no-wait', 'Return after the hosted run is queued').default(undefined))
211
+ .option('--no-wait', 'Return after the hosted run is queued')
184
212
  .option('--interval <seconds>', 'Hosted polling interval (default 5)')
185
213
  .option('--timeout <minutes>', 'Maximum hosted wait (default 35)')
186
- .option('--intent-transcript <file>', 'Coding-agent transcript used as intent evidence')
187
- .option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
188
- .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
189
- .option('--no-open', 'Do not open the live results page')
190
214
  .option('--json', 'Machine-readable run status and results')
191
215
  .addHelpText('after', `
192
- Launch specimens:
193
- posthog-js-478 Replay PostHog/posthog-js PR #478 in matched browser environments (latent-issue mode)
194
- analytics-js-255 Identity routing regression with sandbox bisect
195
- all Run each reproduced specimen as an isolated chapter (default)
216
+ Run inside a git checkout after pushing your current commit. The repository
217
+ comes from origin, head from HEAD, and base from git merge-base HEAD with
218
+ origin's default branch (origin/HEAD, falling back to origin/main).
219
+ Explicit --repo, --base, and --head values override these defaults.
220
+ Intent is optional. Hosted status resumes an existing run.
196
221
 
197
- Repository verification uses the authenticated hosted fleet and resolves refs
198
- once. Intent is optional. Hosted status also resumes these runs.
199
-
200
- For launch specimens, run the local control plane from the Haystack repository:
201
- pnpm dev
222
+ Examples:
223
+ haystack verify
224
+ haystack verify --json
225
+ haystack verify --repo owner/repo --base main --head feature --no-wait
226
+ haystack verify history owner/repo --limit 20
227
+ haystack verify hosted status cv_<48 lowercase hex characters> --wait
228
+ `)
229
+ .action(options => runPublicCommand(() => verifyCommand(options), options.json));
230
+ /**
231
+ * Options for a `verify` subcommand: its own values, plus any option the user
232
+ * passed to an ancestor (`haystack verify --no-wait hosted status ...`).
233
+ * Ancestors' defaults are not inherited. verify's lone --no-wait gets
234
+ * commander's implicit default of true, which would otherwise make
235
+ * `verify hosted status` wait although it waits only with --wait.
236
+ */
237
+ function verifyCommandOptions(command) {
238
+ const commands = [];
239
+ for (let current = command; current; current = current.parent) {
240
+ commands.unshift(current);
241
+ }
242
+ const options = { ...command.opts() };
243
+ for (const current of commands) {
244
+ for (const [name, value] of Object.entries(current.opts())) {
245
+ if (current.getOptionValueSource(name) !== 'default')
246
+ options[name] = value;
247
+ }
248
+ }
249
+ return options;
250
+ }
251
+ verify
252
+ .command('history')
253
+ .description('Read your recent finished hosted verification runs without starting a run')
254
+ .argument('<repository>', 'Exact GitHub owner/repository name')
255
+ .option('--limit <runs>', 'Number of recent finished runs to show, from 1 to 100 (default 20)')
256
+ .option('--account <login>', 'Use a specific saved Haystack account')
257
+ .option('--json', 'Versioned history with execution health and separate bug counts')
258
+ .addHelpText('after', `
259
+ History covers your hosted fleet runs for this repository. Reliability counts
260
+ only these returned runs; it does not measure whether the code is bug-free.
202
261
 
203
262
  Examples:
204
- haystack verify --repo owner/repo --base main --head feature --json
205
- haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
206
- haystack verify posthog-js-478 # start a run, then:
207
- haystack verify reproducibility posthog-js-478 --runs 3
208
- haystack verify watch # block until it finishes (exit 1 = tests failed)
209
- haystack verify status # one-screen verdicts + live environment URLs
210
- haystack verify findings # every problem found, with the readings that moved
211
- haystack verify show latest 3 # one test in full: readings, timeline, SDK log
212
- haystack verify diff latest 3 # base-vs-head screenshot differ (1/2/3 views)
213
- haystack verify open latest 3 --live # open a failing test's live environment
214
- haystack verify cleanup latest 3 # retained-state deletion/reference status
215
- haystack verify refork latest 3 attempt:fix-2 # checkpoint a durable child
216
- haystack verify shell latest 3 # terminal into that live universe
217
- haystack verify artifacts latest # pull screenshots / DOM / video locally
218
- haystack verify sandboxes --kill-stale # reclaim E2B capacity from superseded runs
219
- haystack verify reseal <specimen> # rebuild a sealed bundle + restart control plane
263
+ haystack verify history owner/repo
264
+ haystack verify history owner/repo --limit 50 --account my-login --json
220
265
  `)
221
- .action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
222
- const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
266
+ .action(async (repository, _options, cmd) => {
267
+ const options = verifyCommandOptions(cmd);
268
+ const { verifyHistoryCommand } = await import('./commands/verify-history.js');
269
+ return runPublicCommand(() => verifyHistoryCommand(repository, options), options.json);
270
+ });
223
271
  verify
224
272
  .command('precompute')
225
273
  .description('Precompute reusable verification inputs for the current working tree')
@@ -239,17 +287,16 @@ hostedVerify
239
287
  .command('start')
240
288
  .description('Start an authenticated production verification and wait for its terminal result')
241
289
  .argument('<repository>', 'Exact GitHub owner/repository name')
242
- // `verify` already owns --base/--head for the local specimen launcher.
290
+ // `verify` already owns --base/--head for repository verification.
243
291
  // Re-declaring these as Commander requiredOption values makes the parent
244
292
  // consume them and the child then reject a valid invocation. Runtime
245
293
  // validation below accepts the inherited values and still fails closed.
246
294
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
247
295
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
248
- .option('--intent-file <path>', 'Required for this alias: JSON with exactly problem, goal, and intended_outcomes')
296
+ .option('--intent-file <path>', 'Optional JSON with exactly problem, goal, and intended_outcomes')
249
297
  .option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
250
- .option('--replay-plan-from <run-id>', 'Reuse the exact validated risk plan from a completed owned run')
251
298
  .option('--account <login>', 'Use a specific saved Haystack account')
252
- .option('--no-wait', 'Return after the production workflow is queued')
299
+ .option('--no-wait', 'Return after the hosted run is queued')
253
300
  .option('--interval <seconds>', 'Polling interval while waiting (default 5)')
254
301
  .option('--timeout <minutes>', 'Maximum wait (default 35)')
255
302
  .option('--json', 'Versioned machine-readable run status and terminal output')
@@ -257,18 +304,15 @@ hostedVerify
257
304
  The default idempotency key makes an exact retry return the original run. Pass
258
305
  --idempotency-key with a new stable key only when you intentionally want a
259
306
  fresh production execution of the same commit pair.
260
- --replay-plan-from requires the same repository and exact commit pair and emits
261
- a schema-9 terminal receipt with replay provenance.
262
307
 
263
308
  Examples:
264
309
  haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
265
- haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --replay-plan-from cv_<48 lowercase hex characters>
266
310
  haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --no-wait --json
267
311
  `)
268
312
  .action(async (repository, _options, cmd) => {
269
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
313
+ const options = verifyCommandOptions(cmd);
270
314
  const { verifyHostedStartCommand } = await import('./commands/verify-hosted.js');
271
- return runPublicCommand(() => verifyHostedStartCommand(repository, options), options.json);
315
+ return runPublicCommand(() => verifyHostedStartCommand(repository, { ...options, base: options.base, head: options.head }), options.json);
272
316
  });
273
317
  hostedVerify
274
318
  .command('status')
@@ -284,43 +328,37 @@ Example:
284
328
  haystack verify hosted status cv_<48 lowercase hex characters> --wait --json
285
329
  `)
286
330
  .action(async (runId, _options, cmd) => {
287
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
331
+ const options = verifyCommandOptions(cmd);
288
332
  const { verifyHostedStatusCommand } = await import('./commands/verify-hosted.js');
289
333
  return runPublicCommand(() => verifyHostedStatusCommand(runId, options), options.json);
290
334
  });
291
335
  hostedVerify
292
336
  .command('reproducibility')
293
- .description('Replay one exact hosted risk plan or run fresh plans to report drift')
337
+ .description('Repeat independent fleet runs and report each run receipt')
294
338
  .argument('<repository>', 'Exact GitHub owner/repository name')
295
339
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
296
340
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
297
- .option('--intent-file <path>', 'Required: agent-supplied JSON reused for every hosted run in the series')
298
- .option('--runs <n>', 'Number of sequential production runs (default 3; min 2, max 10)')
341
+ .option('--intent-file <path>', 'Optional intent JSON reused for every fleet run')
342
+ .option('--runs <n>', 'Number of sequential fleet runs (default 3; min 2, max 10)')
299
343
  .option('--series-key <key>', 'Stable series key for resumable idempotent retries')
300
- .option('--fresh-plans', 'Run the planner independently for every run instead of replaying the first plan')
301
344
  .option('--account <login>', 'Use a specific saved Haystack account')
302
345
  .option('--interval <seconds>', 'Polling interval for each run (default 5)')
303
346
  .option('--timeout <minutes>', 'Maximum wait per run (default 35)')
304
- .option('--json', 'Versioned machine-readable reproducibility report')
347
+ .option('--json', 'Machine-readable series with each fleet run receipt')
305
348
  .addHelpText('after', `
306
- The first run creates a fresh risk plan. Every later run replays that exact
307
- validated plan against the same repository and commit pair with a distinct,
308
- series-derived idempotency key. A reproducible verdict requires schema-9 replay
309
- provenance, stable assessment outcomes, and no inconclusive cells.
310
-
311
- With --fresh-plans, every run plans independently. A reproducible verdict then
312
- requires one stable-id plan variant, one assessment variant, no replay
313
- provenance, and no inconclusive cells. Use this mode to detect planner drift;
314
- use the default mode to isolate executor or assessment drift.
349
+ Each run independently plans and verifies the same exact commit pair. The
350
+ series key gives each run a distinct, resumable idempotency key.
351
+ Independent plans do not share an execution identity: the report leaves
352
+ reproducibility unestablished and exits 2. A timeout stops the series and
353
+ includes the command to resume the active run.
315
354
 
316
355
  Example:
317
- haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --intent-file <path> --runs 3
318
- haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --intent-file <path> --runs 3 --fresh-plans
356
+ haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3 --json
319
357
  `)
320
358
  .action(async (repository, _options, cmd) => {
321
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
359
+ const options = verifyCommandOptions(cmd);
322
360
  const { verifyHostedReproducibilityCommand } = await import('./commands/verify-hosted-reproducibility.js');
323
- return runPublicCommand(() => verifyHostedReproducibilityCommand(repository, options), options.json);
361
+ return runPublicCommand(() => verifyHostedReproducibilityCommand(repository, { ...options, base: options.base, head: options.head }), options.json);
324
362
  });
325
363
  hostedVerify
326
364
  .command('mcp')
@@ -332,7 +370,7 @@ No credential, repository source, provider metadata, or sandbox mutation tool
332
370
  is exposed through this production server.
333
371
  `)
334
372
  .action(async (_options, cmd) => {
335
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
373
+ const options = verifyCommandOptions(cmd);
336
374
  try {
337
375
  const { runHostedVerifyMcpServer } = await import('./commands/verify-hosted-mcp.js');
338
376
  await runHostedVerifyMcpServer(options);
@@ -342,48 +380,15 @@ is exposed through this production server.
342
380
  process.exit(1);
343
381
  }
344
382
  });
345
- verify
346
- .command('reproducibility')
347
- .description('Separate frozen-plan drift from executor/judgment nondeterminism')
348
- .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
349
- .option('--runs <n>', 'Number of sequential runs to compare (default 3; min 2, max 20)')
350
- .option('--scope <scope>', 'executor replays one frozen plan (default); planner/full independently replan each run', 'executor')
351
- .option('--plan-source <runId>', 'executor scope: replay this existing structured frozen plan for every run')
352
- .option('--interval <seconds>', 'Polling interval for each run (default 3)')
353
- .option('--timeout <minutes>', 'Per-run timeout (default: wait forever)')
354
- .option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
355
- .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
356
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
357
- .option('--json', 'Machine-readable stable/unstable cell report')
358
- .action(async (specimen, commandOptions, cmd) => {
359
- const options = { ...cmd.optsWithGlobals(), ...commandOptions };
360
- const { verifyReproducibilityCommand } = await import('./commands/verify-reproducibility.js');
361
- return runPublicCommand(() => verifyReproducibilityCommand(specimen, options), options.json);
362
- });
363
- verify
364
- .command('status')
365
- .description('One screen for a run: per-test verdicts, counts, live environments and expiry')
366
- .argument('[run]', verifyRunArg)
367
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
368
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
369
- .option('--json', 'Machine-readable summary')
370
- .action(async (run, _options, cmd) => {
371
- // optsWithGlobals: the parent `verify` command also declares --json/--server
372
- // and commander's default parsing lets it consume those flags even when
373
- // they appear after the subcommand; the merged view (self wins) restores them.
374
- const options = cmd.optsWithGlobals();
375
- const { verifyStatusCommand } = await import('./commands/verify-ops.js');
376
- return runPublicCommand(() => verifyStatusCommand(run, options), options.json);
377
- });
378
383
  verify
379
384
  .command('cases <run>')
380
385
  .description('List cloud verification cases available to explore for 24 hours after completion')
381
386
  .option('--repo <owner/repo>', 'Required exact GitHub repository; uses your Haystack login')
382
387
  .option('--json', 'Machine-readable case IDs and expiry times')
383
388
  .action(async (run, _options, cmd) => {
384
- const options = cmd.optsWithGlobals();
389
+ const options = verifyCommandOptions(cmd);
385
390
  const { verifyCasesCommand } = await import('./commands/verify-explore.js');
386
- return runPublicCommand(() => verifyCasesCommand(run, options), options.json);
391
+ return runPublicCommand(() => verifyCasesCommand(run, { ...options, repo: options.repo }), options.json);
387
392
  });
388
393
  verify
389
394
  .command('explore <run> <case-id>')
@@ -399,210 +404,106 @@ verify
399
404
  .option('--no-wait', 'Return after submitting the action')
400
405
  .option('--json', 'Machine-readable action and command output')
401
406
  .action(async (run, caseId, _options, cmd) => {
402
- const options = cmd.optsWithGlobals();
407
+ const options = verifyCommandOptions(cmd);
403
408
  const { verifyExploreCommand } = await import('./commands/verify-explore.js');
404
- return runPublicCommand(() => verifyExploreCommand(run, caseId, options), options.json);
405
- });
406
- verify
407
- .command('watch')
408
- .description('Follow a run until it finishes; exit 0 all passed, 1 tests failed, 2 run/infra broke')
409
- .argument('[run]', verifyRunArg)
410
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
411
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
412
- .option('--interval <seconds>', 'Poll interval (default 3)')
413
- .option('--timeout <minutes>', 'Give up after this long (default: wait forever)')
414
- .option('--json', 'Suppress streaming; print the final summary as JSON')
415
- .action(async (run, _options, cmd) => {
416
- const options = cmd.optsWithGlobals();
417
- const { verifyWatchCommand } = await import('./commands/verify-ops.js');
418
- return runPublicCommand(() => verifyWatchCommand(run, options), options.json);
419
- });
420
- verify
421
- .command('list')
422
- .description('Runs newest first: chapter, verdict counts, age (control plane, else disk)')
423
- .option('--limit <n>', 'Max runs to show (default 20)')
424
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
425
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
426
- .option('--json', 'Machine-readable list')
427
- .action(async (_options, cmd) => {
428
- const options = cmd.optsWithGlobals();
429
- const { verifyListCommand } = await import('./commands/verify-ops.js');
430
- return runPublicCommand(() => verifyListCommand(options), options.json);
431
- });
432
- verify
433
- .command('findings')
434
- .description('Every problem the run found, in one screen: what failed, which readings moved, live links')
435
- .argument('[run]', verifyRunArg)
436
- .option('--full', 'Print the judge\'s full explanation for each finding')
437
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
438
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
439
- .option('--json', 'Machine-readable findings')
440
- .action(async (run, _options, cmd) => {
441
- const options = cmd.optsWithGlobals();
442
- const { verifyFindingsCommand } = await import('./commands/verify-ops.js');
443
- return runPublicCommand(() => verifyFindingsCommand(run, options), options.json);
444
- });
445
- verify
446
- .command('open')
447
- .description('Open a run artifact (screenshot/video/DOM) or a failing test\'s live environment')
448
- .argument('<run>', verifyRunArg)
449
- .argument('<target>', 'Test selector (ordinal/title), or an artifact file name (….png/.webm/.html)')
450
- .option('--live', 'Open the test\'s live environment URL instead of an artifact')
451
- .option('--side <side>', 'base or head (default head)')
452
- .option('--step <n>', 'Open the screenshot captured after replay step n')
453
- .option('--kind <kind>', 'screenshot | video | dom (default: video, else last screenshot)')
454
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
455
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
456
- .action(async (run, target, _options, cmd) => {
457
- const options = cmd.optsWithGlobals();
458
- const { verifyOpenCommand } = await import('./commands/verify-ops.js');
459
- return runPublicCommand(() => verifyOpenCommand(run, target, options));
460
- });
461
- verify
462
- .command('diff')
463
- .description('Open a base-vs-head screenshot differ for one test (1 what changed / 2 before / 3 after)')
464
- .argument('<run>', verifyRunArg)
465
- .argument('<cell>', 'Test selector: ordinal, cell id, or title substring')
466
- .option('--step <n>', 'Compare only the screenshot captured after replay step n')
467
- .option('--out <dir>', 'Where to build the diff page (default: a temp directory)')
468
- .option('--no-open', 'Build the page but do not open it')
469
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
470
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
471
- .option('--json', 'Machine-readable result (implies --no-open)')
472
- .action(async (run, cell, _options, cmd) => {
473
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
474
- const { verifyDiffCommand } = await import('./commands/verify-ops.js');
475
- return runPublicCommand(() => verifyDiffCommand(run, cell, options), options.json);
476
- });
477
- verify
478
- .command('cleanup')
479
- .description('Show exact retained-state references, provider deletion, and cleanup-SLO status')
480
- .argument('<run>', verifyRunArg)
481
- .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
482
- .option('--side <side>', 'base or head (default head)')
483
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
484
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
485
- .option('--json', 'Machine-readable cleanup status')
486
- .action(async (run, cell, _options, cmd) => {
487
- const options = cmd.optsWithGlobals();
488
- const { verifyCleanupCommand } = await import('./commands/verify-ops.js');
489
- return runPublicCommand(() => verifyCleanupCommand(run, cell, options), options.json);
409
+ return runPublicCommand(() => verifyExploreCommand(run, caseId, { ...options, repo: options.repo }), options.json);
490
410
  });
491
- verify
492
- .command('reopen')
493
- .description('Attach fresh E2B compute to retained Archil state from a test or exact fork ID')
494
- .argument('<source>', 'Run selector, or an exact archil:…:branch:… fork ID')
495
- .argument('[cell]', 'Stable test selector for a run: ordinal, riskCellId, or exact planner cellKey')
496
- .option('--side <side>', 'base or head (default head)')
497
- .option('--open-browser', 'Open the restarted application in a browser')
498
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
499
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
500
- .option('--json', 'Machine-readable attachment details')
501
- .action(async (source, cell, _options, cmd) => {
502
- const options = cmd.optsWithGlobals();
503
- const { verifyReopenCommand } = await import('./commands/verify-ops.js');
504
- return runPublicCommand(() => verifyReopenCommand(source, cell, options), options.json);
505
- });
506
- verify
507
- .command('refork')
508
- .description('Checkpoint a reopened Archil universe into a durable child fork')
509
- .argument('<source>', 'Run selector, or an exact archil:…:branch:… parent fork ID')
510
- .argument('<cell-or-child-key>', 'Stable test selector for a run, or child key for an exact fork ID')
511
- .argument('[child-key]', 'Structured stable child key when source is a run')
512
- .option('--side <side>', 'base or head (default head; only applies to a run)')
513
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
514
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
515
- .option('--json', 'Machine-readable parent, child, checkpoint, and expiry')
516
- .action(async (source, cellOrChildKey, childKey, _options, cmd) => {
517
- const options = cmd.optsWithGlobals();
518
- const { verifyReforkCommand } = await import('./commands/verify-ops.js');
519
- return runPublicCommand(() => verifyReforkCommand(source, cellOrChildKey, childKey, options), options.json);
520
- });
521
- verify
522
- .command('shell')
523
- .description('Terminal into a failing test\'s live universe (e2b sandbox connect)')
524
- .argument('<run>', verifyRunArg)
525
- .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
526
- .option('--side <side>', 'base or head (default head; only failing tests keep a universe alive)')
527
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
528
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
529
- .action(async (run, cell, _options, cmd) => {
530
- const options = cmd.optsWithGlobals();
531
- const { verifyShellCommand } = await import('./commands/verify-ops.js');
532
- return runPublicCommand(() => verifyShellCommand(run, cell, options));
533
- });
534
- verify
535
- .command('show')
536
- .description('One test in full: finding, changed readings, request timeline, SDK log lines')
537
- .argument('<run>', verifyRunArg)
538
- .argument('<cell>', 'Test ordinal, cell id, planner cell key, or title substring')
539
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
540
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
541
- .option('--no-timeline', 'Skip the request timeline')
542
- .option('--json', 'Machine-readable detail')
543
- .action(async (run, cell, _options, cmd) => {
544
- const options = cmd.optsWithGlobals();
545
- const { verifyShowCommand } = await import('./commands/verify-ops.js');
546
- return runPublicCommand(() => verifyShowCommand(run, cell, options), options.json);
547
- });
548
- verify
549
- .command('artifacts')
550
- .description('Pull a run\'s screenshots, DOM snapshots and videos to a local directory')
551
- .argument('<run>', verifyRunArg)
552
- .option('--out <dir>', 'Destination (default: ./verify-artifacts/<runId>)')
553
- .option('--cell <selector>', 'Only artifacts for one test (ordinal, cell id, or title substring)')
554
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
555
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
556
- .option('--json', 'Machine-readable result')
557
- .action(async (run, _options, cmd) => {
558
- const options = cmd.optsWithGlobals();
559
- const { verifyArtifactsCommand } = await import('./commands/verify-ops.js');
560
- return runPublicCommand(() => verifyArtifactsCommand(run, options), options.json);
411
+ const caseBatch = program
412
+ .command('case-batch')
413
+ .description('Submit and poll product-fuzzing case batches on the hosted coordinator (CASE-BATCH-V1)');
414
+ caseBatch
415
+ .command('submit')
416
+ .description('Submit an ordered batch of already-generated cases against one frozen world pair')
417
+ .requiredOption('--repository <owner/repo>', 'Exact GitHub owner/repository name')
418
+ .requiredOption('--base <sha>', 'Exact 40-character lowercase base commit SHA')
419
+ .requiredOption('--head <sha>', 'Exact 40-character lowercase head commit SHA')
420
+ .requiredOption('--cases <file>', 'JSON file: the ordered ProductCase array (or { "cases": [...] })')
421
+ .requiredOption('--combinations <file>', 'JSON file: the combination space each case start resolves against')
422
+ .option('--base-world <id>', 'Existing frozen base world (frz_ plus 16 hex); omit to prepare it')
423
+ .option('--head-world <id>', 'Existing frozen head world (frz_ plus 16 hex); omit to prepare it')
424
+ .option('--max-concurrent <n>', 'Concurrency ceiling, 1-500 (default: the case count, capped at 500)')
425
+ .option('--per-case-wall-ms <ms>', 'Per-case wall budget, 1000-600000 (default 120000)')
426
+ .option('--total-budget-ms <ms>', 'Launch budget for the batch, 10000-1800000 (default 1800000)')
427
+ .option('--idempotency-key <key>', 'Stable retry key (default: deterministic from the whole submission)')
428
+ .option('--source-patch <file>', 'JSON file with the precompute-captured { cacheKey, patchSha256, patchGzBase64 }')
429
+ .option('--account <login>', 'Use a specific saved Haystack account')
430
+ .option('--json', 'Versioned machine-readable admission receipt')
431
+ .addHelpText('after', `
432
+ Inputs travel inline and the whole request is bounded at 8 MiB. No source patch
433
+ is sent unless --source-patch names one.
434
+
435
+ --max-concurrent is a ceiling, not a reservation: admitted concurrency is the
436
+ smaller of it and what the fleet's free capacity allows, and the batch's own
437
+ receipt reports both. --per-case-wall-ms kills a case that overruns and records
438
+ it as UNKNOWN/wall-budget rather than dropping it. --total-budget-ms bounds
439
+ launches only; cases already launched run to their own wall budget.
440
+
441
+ Example:
442
+ haystack case-batch submit --repository owner/repo --base <sha> --head <sha> \\
443
+ --cases cases.json --combinations space.json --max-concurrent 200 --json
444
+ `)
445
+ .action(async (options) => {
446
+ const { caseBatchSubmitCommand } = await import('./commands/case-batch.js');
447
+ return runPublicCommand(() => caseBatchSubmitCommand(options), options.json);
561
448
  });
562
- verify
563
- .command('sandboxes')
564
- .description('E2B sandbox inventory with run attribution; --kill-stale reclaims superseded ones')
565
- .option('--kill-stale', 'Kill verifier-attributed sandboxes superseded by newer runs (never unattributed ones)')
566
- .option('--kill-run <runId>', 'Kill every sandbox attributed to one run')
567
- .option('--server <url>', 'Control-plane URL, preferred over direct E2B access (default: http://127.0.0.1:3000)')
568
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
569
- .option('--json', 'Machine-readable inventory')
570
- .action(async (_options, cmd) => {
571
- const options = cmd.optsWithGlobals();
572
- const { verifySandboxesCommand } = await import('./commands/verify-sandboxes.js');
573
- return runPublicCommand(() => verifySandboxesCommand(options), options.json);
449
+ caseBatch
450
+ .command('status')
451
+ .description('Read a case batch and every per-case outcome, optionally watching until terminal')
452
+ .argument('<run-id>', 'Case batch run id (cv_ followed by 48 lowercase hex characters)')
453
+ .requiredOption('--repository <owner/repo>', 'Exact GitHub owner/repository name')
454
+ .option('--watch', 'Keep polling until the batch is terminal or --max-wall expires')
455
+ .option('--interval <seconds>', 'Polling interval while watching, 1-300 (default 10)')
456
+ .option('--max-wall <minutes>', 'Maximum total polling wall, 1-240 (default 45)')
457
+ .option('--account <login>', 'Use a specific saved Haystack account')
458
+ .option('--json', 'Versioned machine-readable snapshot with every per-case row')
459
+ .addHelpText('after', `
460
+ Case rows are read with bounded pagination and the cursor is followed to the
461
+ end of the list. A batch that ends incomplete still prints every per-case
462
+ outcome; the command then exits 2 with a summary of what is missing. Exit 2
463
+ also covers a cancelled batch, unproven cleanup, and a wait that hits
464
+ --max-wall.
465
+
466
+ Example:
467
+ haystack case-batch status cv_<48 hex> --repository owner/repo --watch --json
468
+ `)
469
+ .action(async (runId, options) => {
470
+ const { caseBatchStatusCommand } = await import('./commands/case-batch.js');
471
+ return runPublicCommand(() => caseBatchStatusCommand(runId, options), options.json);
574
472
  });
575
- verify
576
- .command('reseal')
577
- .description('Rebuild a sealed specimen bundle (guarded) and restart the control plane onto it')
578
- .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
579
- .option('--source <dir>', 'Specimen source directory holding reseal.sh')
580
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
581
- .option('--repo-root <path>', 'Repository holding demo/cloud-verifier (default: walk up from cwd)')
582
- .option('--no-restart', 'Only rebuild the bundle; leave the running server on the old checkout')
583
- .action(async (specimen, _options, cmd) => {
584
- const options = cmd.optsWithGlobals();
585
- const { verifyResealCommand } = await import('./commands/verify-reseal.js');
586
- return runPublicCommand(() => verifyResealCommand(specimen, options));
473
+ caseBatch
474
+ .command('cancel')
475
+ .description('Request cancellation of an owned case batch')
476
+ .argument('<run-id>', 'Case batch run id (cv_ followed by 48 lowercase hex characters)')
477
+ .requiredOption('--repository <owner/repo>', 'Exact GitHub owner/repository name')
478
+ .option('--account <login>', 'Use a specific saved Haystack account')
479
+ .option('--json', 'Versioned machine-readable cancellation receipt')
480
+ .addHelpText('after', `
481
+ A claimed batch stays draining until its current owner proves physical VM
482
+ cleanup and posts its terminal checkpoint.
483
+ `)
484
+ .action(async (runId, options) => {
485
+ const { caseBatchCancelCommand } = await import('./commands/case-batch.js');
486
+ return runPublicCommand(() => caseBatchCancelCommand(runId, options), options.json);
587
487
  });
588
- verify
589
- .command('mcp')
590
- .description('Run a stdio MCP server exposing the verifier to agents (verify_start/verify_wait/…)')
591
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
592
- .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
593
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
594
- .action(async (_options, cmd) => {
595
- const options = cmd.optsWithGlobals();
596
- try {
597
- // Lazy import, same reasoning as the top-level `mcp` command: only this
598
- // path needs @modelcontextprotocol/sdk.
599
- const { runVerifyMcpServer } = await import('./commands/verify-mcp.js');
600
- await runVerifyMcpServer(options);
601
- }
602
- catch (err) {
603
- console.error(chalk.red('verify mcp server failed:'), err instanceof Error ? err.message : err);
604
- process.exit(1);
605
- }
488
+ caseBatch
489
+ .command('bundle')
490
+ .description('Download a case batch replay bundle, verifying every artifact digest')
491
+ .argument('<run-id>', 'Case batch run id (cv_ followed by 48 lowercase hex characters)')
492
+ .requiredOption('--repository <owner/repo>', 'Exact GitHub owner/repository name')
493
+ .requiredOption('--out <dir>', 'Directory to write the verified bundle into')
494
+ .option('--account <login>', 'Use a specific saved Haystack account')
495
+ .option('--json', 'Versioned machine-readable list of verified artifacts')
496
+ .addHelpText('after', `
497
+ The manifest is fetched first and checked against the digest the service sealed
498
+ it under; that sealed digest is required, because the manifest is the one
499
+ artifact with no parent digest to check it against. Every artifact the manifest
500
+ lists (and every artifact those pages list) is then fetched and checked against
501
+ the manifest's own sha256 and byte length before it is written. Any failure
502
+ stops the command; it never writes unverified bytes.
503
+ `)
504
+ .action(async (runId, options) => {
505
+ const { caseBatchBundleCommand } = await import('./commands/case-batch.js');
506
+ return runPublicCommand(() => caseBatchBundleCommand(runId, options), options.json);
606
507
  });
607
508
  program
608
509
  .command('login')
@@ -901,6 +802,8 @@ program
901
802
  // keeps retired auto-fix/verification wording out of the CLI surface).
902
803
  .description('Run a blind design-by-execution review of a PR')
903
804
  .option('--json', 'Machine-readable output')
805
+ .option('--history', 'Show recent execution reliability for this repository without starting a run')
806
+ .option('--limit <attempts>', 'Maximum recorded terminal attempts to show with --history (1–100, default 20)')
904
807
  .option('--no-wait', 'Return after the run is admitted instead of waiting for its verdict')
905
808
  .option('--run <run-id>', 'Resume and wait for an existing exact run id')
906
809
  .option('--poll-interval <seconds>', 'Status polling interval', '3')
@@ -921,6 +824,11 @@ outside the registered intent.
921
824
  The command waits for a behavioral verdict by default. Use --no-wait to
922
825
  return the run id immediately, or --run <run-id> to resume waiting.
923
826
 
827
+ Read recent execution reliability across this repository's PRs:
828
+ haystack design-verify owner/repo#123 --history --limit 20
829
+ History covers recorded terminal attempts, including partial or failed runs;
830
+ it does not measure bug-detection accuracy or whether changes are bug-free.
831
+
924
832
  After a completed run, inspect or replay one retained case universe without
925
833
  receiving provider credentials:
926
834
  haystack design-verify owner/repo#123 --run RUN --case case-00001 --universe before --replay
@@ -1257,12 +1165,14 @@ hooks
1257
1165
  .command('install-session')
1258
1166
  .description('Install session-start and Stop hooks for coding CLIs')
1259
1167
  .option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
1168
+ .option('--shared', 'Write Claude Code hooks to the committed .claude/settings.json instead of settings.local.json')
1260
1169
  .addHelpText('after', `
1261
1170
  Configures your coding CLI to run \`haystack triage --hook\` on session start.
1262
1171
  This shows pending PR analysis results when you open a new terminal session.
1263
1172
  Claude Code also runs \`haystack verify precompute --hook\` when a session stops.
1264
1173
 
1265
- Claude Code: Native SessionStart and Stop hooks (.claude/settings.json)
1174
+ Claude Code: Native SessionStart and Stop hooks (.claude/settings.local.json;
1175
+ --shared writes the committed .claude/settings.json)
1266
1176
  Codex CLI: AGENTS.md instructions
1267
1177
  Gemini CLI: GEMINI.md instructions
1268
1178
 
@@ -1520,4 +1430,10 @@ program
1520
1430
  if (process.argv.length === 2) {
1521
1431
  program.help();
1522
1432
  }
1523
- program.parse();
1433
+ await program.parseAsync();
1434
+ // Runs only after the command's action has settled; --version, --help and
1435
+ // commands that call process.exit never reach it. See src/utils/update-check.ts.
1436
+ {
1437
+ const { notifyIfUpdateAvailable } = await import('./utils/update-check.js');
1438
+ await notifyIfUpdateAvailable(getVersion());
1439
+ }