@haystackeditor/cli 0.15.31 → 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 (133) 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/dismiss.js +2 -1
  12. package/dist/commands/hooks.js +66 -7
  13. package/dist/commands/install-session-hooks.js +131 -44
  14. package/dist/commands/policy.js +32 -45
  15. package/dist/commands/precompute-delivery.js +6 -1
  16. package/dist/commands/scaffold-provisional-universe.js +8 -10
  17. package/dist/commands/setup.js +32 -10
  18. package/dist/commands/submit.js +7 -2
  19. package/dist/commands/telemetry.js +17 -2
  20. package/dist/commands/triage.js +2 -1
  21. package/dist/commands/verify-explore.js +1 -0
  22. package/dist/commands/verify-hosted-mcp.js +3 -12
  23. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  24. package/dist/commands/verify-hosted.js +66 -168
  25. package/dist/commands/verify-precompute.js +57 -10
  26. package/dist/commands/verify.js +51 -139
  27. package/dist/index.js +238 -350
  28. package/dist/lazy.js +8 -0
  29. package/dist/schema.js +1 -0
  30. package/dist/tools/detect.js +3 -24
  31. package/dist/triage/prompts.js +46 -45
  32. package/dist/triage/runner.js +119 -33
  33. package/dist/types.js +3 -3
  34. package/dist/utils/auth.js +14 -2
  35. package/dist/utils/git.js +60 -29
  36. package/dist/utils/github-api.js +14 -1
  37. package/dist/utils/haystack-api.js +26 -7
  38. package/dist/utils/hooks.js +43 -6
  39. package/dist/utils/prompter.js +25 -10
  40. package/dist/utils/safe-write.js +31 -0
  41. package/dist/utils/secret-paths.js +115 -0
  42. package/dist/utils/secrets.js +0 -1
  43. package/dist/utils/telemetry.js +34 -14
  44. package/dist/utils/update-check.js +151 -0
  45. package/package.json +18 -14
  46. package/schemas/case-batch.v1.json +184 -0
  47. package/schemas/cloud-verifier.v1.json +55 -100
  48. package/dist/commands/ask.d.ts +0 -14
  49. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  50. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  51. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  52. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  53. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  54. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  55. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  56. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  57. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  58. package/dist/commands/config.d.ts +0 -46
  59. package/dist/commands/design-verify.d.ts +0 -33
  60. package/dist/commands/dismiss.d.ts +0 -29
  61. package/dist/commands/hooks.d.ts +0 -13
  62. package/dist/commands/inbox.d.ts +0 -65
  63. package/dist/commands/init.d.ts +0 -10
  64. package/dist/commands/install-session-hooks.d.ts +0 -17
  65. package/dist/commands/login.d.ts +0 -8
  66. package/dist/commands/mcp.d.ts +0 -1
  67. package/dist/commands/policy.d.ts +0 -31
  68. package/dist/commands/pr-status.d.ts +0 -144
  69. package/dist/commands/pr.d.ts +0 -40
  70. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  71. package/dist/commands/precompute-delivery.d.ts +0 -20
  72. package/dist/commands/prepare-universe-review.d.ts +0 -115
  73. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  74. package/dist/commands/request-review.d.ts +0 -26
  75. package/dist/commands/review.d.ts +0 -25
  76. package/dist/commands/rules.d.ts +0 -4
  77. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  78. package/dist/commands/schema-cmd.d.ts +0 -2
  79. package/dist/commands/setup.d.ts +0 -28
  80. package/dist/commands/skills.d.ts +0 -8
  81. package/dist/commands/status.d.ts +0 -4
  82. package/dist/commands/submit.d.ts +0 -30
  83. package/dist/commands/system-map.d.ts +0 -42
  84. package/dist/commands/telemetry.d.ts +0 -53
  85. package/dist/commands/tokens.d.ts +0 -14
  86. package/dist/commands/triage.d.ts +0 -35
  87. package/dist/commands/verify-core.d.ts +0 -449
  88. package/dist/commands/verify-core.js +0 -789
  89. package/dist/commands/verify-explore.d.ts +0 -14
  90. package/dist/commands/verify-history.d.ts +0 -14
  91. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  92. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  93. package/dist/commands/verify-hosted.d.ts +0 -92
  94. package/dist/commands/verify-mcp.d.ts +0 -8
  95. package/dist/commands/verify-mcp.js +0 -517
  96. package/dist/commands/verify-ops.d.ts +0 -158
  97. package/dist/commands/verify-ops.js +0 -1148
  98. package/dist/commands/verify-precompute.d.ts +0 -30
  99. package/dist/commands/verify-reproducibility.d.ts +0 -85
  100. package/dist/commands/verify-reproducibility.js +0 -494
  101. package/dist/commands/verify-reseal.d.ts +0 -9
  102. package/dist/commands/verify-reseal.js +0 -148
  103. package/dist/commands/verify-sandboxes.d.ts +0 -95
  104. package/dist/commands/verify-sandboxes.js +0 -352
  105. package/dist/commands/verify.d.ts +0 -28
  106. package/dist/commands/webhooks.d.ts +0 -30
  107. package/dist/index.d.ts +0 -22
  108. package/dist/schema.d.ts +0 -28
  109. package/dist/states.d.ts +0 -29
  110. package/dist/tools/detect.d.ts +0 -50
  111. package/dist/triage/prompts.d.ts +0 -24
  112. package/dist/triage/runner.d.ts +0 -34
  113. package/dist/triage/types.d.ts +0 -42
  114. package/dist/types/verify-history.d.ts +0 -38
  115. package/dist/types.d.ts +0 -1684
  116. package/dist/utils/action-output.d.ts +0 -24
  117. package/dist/utils/analysis-api.d.ts +0 -187
  118. package/dist/utils/auth.d.ts +0 -79
  119. package/dist/utils/config.d.ts +0 -24
  120. package/dist/utils/design-verifier-api.d.ts +0 -208
  121. package/dist/utils/design-verifier-history.d.ts +0 -46
  122. package/dist/utils/design-verifier-result.d.ts +0 -74
  123. package/dist/utils/detect.d.ts +0 -43
  124. package/dist/utils/git.d.ts +0 -135
  125. package/dist/utils/github-api.d.ts +0 -104
  126. package/dist/utils/haystack-api.d.ts +0 -37
  127. package/dist/utils/hooks.d.ts +0 -12
  128. package/dist/utils/pending-state.d.ts +0 -40
  129. package/dist/utils/pr-ref.d.ts +0 -27
  130. package/dist/utils/prompter.d.ts +0 -85
  131. package/dist/utils/secrets.d.ts +0 -47
  132. package/dist/utils/telemetry.d.ts +0 -19
  133. /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,55 +201,53 @@ 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)
196
-
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
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.
202
221
 
203
222
  Examples:
204
- haystack verify --repo owner/repo --base main --head feature --json
223
+ haystack verify
224
+ haystack verify --json
225
+ haystack verify --repo owner/repo --base main --head feature --no-wait
205
226
  haystack verify history owner/repo --limit 20
206
- haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
207
- haystack verify posthog-js-478 # start a run, then:
208
- haystack verify reproducibility posthog-js-478 --runs 3
209
- haystack verify watch # block until it finishes (exit 1 = tests failed)
210
- haystack verify status # one-screen verdicts + live environment URLs
211
- haystack verify findings # every problem found, with the readings that moved
212
- haystack verify show latest 3 # one test in full: readings, timeline, SDK log
213
- haystack verify diff latest 3 # base-vs-head screenshot differ (1/2/3 views)
214
- haystack verify open latest 3 --live # open a failing test's live environment
215
- haystack verify cleanup latest 3 # retained-state deletion/reference status
216
- haystack verify refork latest 3 attempt:fix-2 # checkpoint a durable child
217
- haystack verify shell latest 3 # terminal into that live universe
218
- haystack verify artifacts latest # pull screenshots / DOM / video locally
219
- haystack verify sandboxes --kill-stale # reclaim E2B capacity from superseded runs
220
- haystack verify reseal <specimen> # rebuild a sealed bundle + restart control plane
227
+ haystack verify hosted status cv_<48 lowercase hex characters> --wait
221
228
  `)
222
- .action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
223
- const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
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
+ }
224
251
  verify
225
252
  .command('history')
226
253
  .description('Read your recent finished hosted verification runs without starting a run')
@@ -237,7 +264,7 @@ Examples:
237
264
  haystack verify history owner/repo --limit 50 --account my-login --json
238
265
  `)
239
266
  .action(async (repository, _options, cmd) => {
240
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
267
+ const options = verifyCommandOptions(cmd);
241
268
  const { verifyHistoryCommand } = await import('./commands/verify-history.js');
242
269
  return runPublicCommand(() => verifyHistoryCommand(repository, options), options.json);
243
270
  });
@@ -260,17 +287,16 @@ hostedVerify
260
287
  .command('start')
261
288
  .description('Start an authenticated production verification and wait for its terminal result')
262
289
  .argument('<repository>', 'Exact GitHub owner/repository name')
263
- // `verify` already owns --base/--head for the local specimen launcher.
290
+ // `verify` already owns --base/--head for repository verification.
264
291
  // Re-declaring these as Commander requiredOption values makes the parent
265
292
  // consume them and the child then reject a valid invocation. Runtime
266
293
  // validation below accepts the inherited values and still fails closed.
267
294
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
268
295
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
269
- .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')
270
297
  .option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
271
- .option('--replay-plan-from <run-id>', 'Reuse the exact validated risk plan from a completed owned run')
272
298
  .option('--account <login>', 'Use a specific saved Haystack account')
273
- .option('--no-wait', 'Return after the production workflow is queued')
299
+ .option('--no-wait', 'Return after the hosted run is queued')
274
300
  .option('--interval <seconds>', 'Polling interval while waiting (default 5)')
275
301
  .option('--timeout <minutes>', 'Maximum wait (default 35)')
276
302
  .option('--json', 'Versioned machine-readable run status and terminal output')
@@ -278,18 +304,15 @@ hostedVerify
278
304
  The default idempotency key makes an exact retry return the original run. Pass
279
305
  --idempotency-key with a new stable key only when you intentionally want a
280
306
  fresh production execution of the same commit pair.
281
- --replay-plan-from requires the same repository and exact commit pair and emits
282
- a schema-9 terminal receipt with replay provenance.
283
307
 
284
308
  Examples:
285
309
  haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
286
- haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --replay-plan-from cv_<48 lowercase hex characters>
287
310
  haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --no-wait --json
288
311
  `)
289
312
  .action(async (repository, _options, cmd) => {
290
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
313
+ const options = verifyCommandOptions(cmd);
291
314
  const { verifyHostedStartCommand } = await import('./commands/verify-hosted.js');
292
- return runPublicCommand(() => verifyHostedStartCommand(repository, options), options.json);
315
+ return runPublicCommand(() => verifyHostedStartCommand(repository, { ...options, base: options.base, head: options.head }), options.json);
293
316
  });
294
317
  hostedVerify
295
318
  .command('status')
@@ -305,43 +328,37 @@ Example:
305
328
  haystack verify hosted status cv_<48 lowercase hex characters> --wait --json
306
329
  `)
307
330
  .action(async (runId, _options, cmd) => {
308
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
331
+ const options = verifyCommandOptions(cmd);
309
332
  const { verifyHostedStatusCommand } = await import('./commands/verify-hosted.js');
310
333
  return runPublicCommand(() => verifyHostedStatusCommand(runId, options), options.json);
311
334
  });
312
335
  hostedVerify
313
336
  .command('reproducibility')
314
- .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')
315
338
  .argument('<repository>', 'Exact GitHub owner/repository name')
316
339
  .option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
317
340
  .option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
318
- .option('--intent-file <path>', 'Required: agent-supplied JSON reused for every hosted run in the series')
319
- .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)')
320
343
  .option('--series-key <key>', 'Stable series key for resumable idempotent retries')
321
- .option('--fresh-plans', 'Run the planner independently for every run instead of replaying the first plan')
322
344
  .option('--account <login>', 'Use a specific saved Haystack account')
323
345
  .option('--interval <seconds>', 'Polling interval for each run (default 5)')
324
346
  .option('--timeout <minutes>', 'Maximum wait per run (default 35)')
325
- .option('--json', 'Versioned machine-readable reproducibility report')
347
+ .option('--json', 'Machine-readable series with each fleet run receipt')
326
348
  .addHelpText('after', `
327
- The first run creates a fresh risk plan. Every later run replays that exact
328
- validated plan against the same repository and commit pair with a distinct,
329
- series-derived idempotency key. A reproducible verdict requires schema-9 replay
330
- provenance, stable assessment outcomes, and no inconclusive cells.
331
-
332
- With --fresh-plans, every run plans independently. A reproducible verdict then
333
- requires one stable-id plan variant, one assessment variant, no replay
334
- provenance, and no inconclusive cells. Use this mode to detect planner drift;
335
- 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.
336
354
 
337
355
  Example:
338
- haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --intent-file <path> --runs 3
339
- 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
340
357
  `)
341
358
  .action(async (repository, _options, cmd) => {
342
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
359
+ const options = verifyCommandOptions(cmd);
343
360
  const { verifyHostedReproducibilityCommand } = await import('./commands/verify-hosted-reproducibility.js');
344
- return runPublicCommand(() => verifyHostedReproducibilityCommand(repository, options), options.json);
361
+ return runPublicCommand(() => verifyHostedReproducibilityCommand(repository, { ...options, base: options.base, head: options.head }), options.json);
345
362
  });
346
363
  hostedVerify
347
364
  .command('mcp')
@@ -353,7 +370,7 @@ No credential, repository source, provider metadata, or sandbox mutation tool
353
370
  is exposed through this production server.
354
371
  `)
355
372
  .action(async (_options, cmd) => {
356
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
373
+ const options = verifyCommandOptions(cmd);
357
374
  try {
358
375
  const { runHostedVerifyMcpServer } = await import('./commands/verify-hosted-mcp.js');
359
376
  await runHostedVerifyMcpServer(options);
@@ -363,48 +380,15 @@ is exposed through this production server.
363
380
  process.exit(1);
364
381
  }
365
382
  });
366
- verify
367
- .command('reproducibility')
368
- .description('Separate frozen-plan drift from executor/judgment nondeterminism')
369
- .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
370
- .option('--runs <n>', 'Number of sequential runs to compare (default 3; min 2, max 20)')
371
- .option('--scope <scope>', 'executor replays one frozen plan (default); planner/full independently replan each run', 'executor')
372
- .option('--plan-source <runId>', 'executor scope: replay this existing structured frozen plan for every run')
373
- .option('--interval <seconds>', 'Polling interval for each run (default 3)')
374
- .option('--timeout <minutes>', 'Per-run timeout (default: wait forever)')
375
- .option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
376
- .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
377
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
378
- .option('--json', 'Machine-readable stable/unstable cell report')
379
- .action(async (specimen, commandOptions, cmd) => {
380
- const options = { ...cmd.optsWithGlobals(), ...commandOptions };
381
- const { verifyReproducibilityCommand } = await import('./commands/verify-reproducibility.js');
382
- return runPublicCommand(() => verifyReproducibilityCommand(specimen, options), options.json);
383
- });
384
- verify
385
- .command('status')
386
- .description('One screen for a run: per-test verdicts, counts, live environments and expiry')
387
- .argument('[run]', verifyRunArg)
388
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
389
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
390
- .option('--json', 'Machine-readable summary')
391
- .action(async (run, _options, cmd) => {
392
- // optsWithGlobals: the parent `verify` command also declares --json/--server
393
- // and commander's default parsing lets it consume those flags even when
394
- // they appear after the subcommand; the merged view (self wins) restores them.
395
- const options = cmd.optsWithGlobals();
396
- const { verifyStatusCommand } = await import('./commands/verify-ops.js');
397
- return runPublicCommand(() => verifyStatusCommand(run, options), options.json);
398
- });
399
383
  verify
400
384
  .command('cases <run>')
401
385
  .description('List cloud verification cases available to explore for 24 hours after completion')
402
386
  .option('--repo <owner/repo>', 'Required exact GitHub repository; uses your Haystack login')
403
387
  .option('--json', 'Machine-readable case IDs and expiry times')
404
388
  .action(async (run, _options, cmd) => {
405
- const options = cmd.optsWithGlobals();
389
+ const options = verifyCommandOptions(cmd);
406
390
  const { verifyCasesCommand } = await import('./commands/verify-explore.js');
407
- return runPublicCommand(() => verifyCasesCommand(run, options), options.json);
391
+ return runPublicCommand(() => verifyCasesCommand(run, { ...options, repo: options.repo }), options.json);
408
392
  });
409
393
  verify
410
394
  .command('explore <run> <case-id>')
@@ -420,210 +404,106 @@ verify
420
404
  .option('--no-wait', 'Return after submitting the action')
421
405
  .option('--json', 'Machine-readable action and command output')
422
406
  .action(async (run, caseId, _options, cmd) => {
423
- const options = cmd.optsWithGlobals();
407
+ const options = verifyCommandOptions(cmd);
424
408
  const { verifyExploreCommand } = await import('./commands/verify-explore.js');
425
- return runPublicCommand(() => verifyExploreCommand(run, caseId, options), options.json);
426
- });
427
- verify
428
- .command('watch')
429
- .description('Follow a run until it finishes; exit 0 all passed, 1 tests failed, 2 run/infra broke')
430
- .argument('[run]', verifyRunArg)
431
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
432
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
433
- .option('--interval <seconds>', 'Poll interval (default 3)')
434
- .option('--timeout <minutes>', 'Give up after this long (default: wait forever)')
435
- .option('--json', 'Suppress streaming; print the final summary as JSON')
436
- .action(async (run, _options, cmd) => {
437
- const options = cmd.optsWithGlobals();
438
- const { verifyWatchCommand } = await import('./commands/verify-ops.js');
439
- return runPublicCommand(() => verifyWatchCommand(run, options), options.json);
440
- });
441
- verify
442
- .command('list')
443
- .description('Runs newest first: chapter, verdict counts, age (control plane, else disk)')
444
- .option('--limit <n>', 'Max runs to show (default 20)')
445
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
446
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
447
- .option('--json', 'Machine-readable list')
448
- .action(async (_options, cmd) => {
449
- const options = cmd.optsWithGlobals();
450
- const { verifyListCommand } = await import('./commands/verify-ops.js');
451
- return runPublicCommand(() => verifyListCommand(options), options.json);
452
- });
453
- verify
454
- .command('findings')
455
- .description('Every problem the run found, in one screen: what failed, which readings moved, live links')
456
- .argument('[run]', verifyRunArg)
457
- .option('--full', 'Print the judge\'s full explanation for each finding')
458
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
459
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
460
- .option('--json', 'Machine-readable findings')
461
- .action(async (run, _options, cmd) => {
462
- const options = cmd.optsWithGlobals();
463
- const { verifyFindingsCommand } = await import('./commands/verify-ops.js');
464
- return runPublicCommand(() => verifyFindingsCommand(run, options), options.json);
465
- });
466
- verify
467
- .command('open')
468
- .description('Open a run artifact (screenshot/video/DOM) or a failing test\'s live environment')
469
- .argument('<run>', verifyRunArg)
470
- .argument('<target>', 'Test selector (ordinal/title), or an artifact file name (….png/.webm/.html)')
471
- .option('--live', 'Open the test\'s live environment URL instead of an artifact')
472
- .option('--side <side>', 'base or head (default head)')
473
- .option('--step <n>', 'Open the screenshot captured after replay step n')
474
- .option('--kind <kind>', 'screenshot | video | dom (default: video, else last screenshot)')
475
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
476
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
477
- .action(async (run, target, _options, cmd) => {
478
- const options = cmd.optsWithGlobals();
479
- const { verifyOpenCommand } = await import('./commands/verify-ops.js');
480
- return runPublicCommand(() => verifyOpenCommand(run, target, options));
481
- });
482
- verify
483
- .command('diff')
484
- .description('Open a base-vs-head screenshot differ for one test (1 what changed / 2 before / 3 after)')
485
- .argument('<run>', verifyRunArg)
486
- .argument('<cell>', 'Test selector: ordinal, cell id, or title substring')
487
- .option('--step <n>', 'Compare only the screenshot captured after replay step n')
488
- .option('--out <dir>', 'Where to build the diff page (default: a temp directory)')
489
- .option('--no-open', 'Build the page but do not open it')
490
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
491
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
492
- .option('--json', 'Machine-readable result (implies --no-open)')
493
- .action(async (run, cell, _options, cmd) => {
494
- const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
495
- const { verifyDiffCommand } = await import('./commands/verify-ops.js');
496
- return runPublicCommand(() => verifyDiffCommand(run, cell, options), options.json);
497
- });
498
- verify
499
- .command('cleanup')
500
- .description('Show exact retained-state references, provider deletion, and cleanup-SLO status')
501
- .argument('<run>', verifyRunArg)
502
- .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
503
- .option('--side <side>', 'base or head (default head)')
504
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
505
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
506
- .option('--json', 'Machine-readable cleanup status')
507
- .action(async (run, cell, _options, cmd) => {
508
- const options = cmd.optsWithGlobals();
509
- const { verifyCleanupCommand } = await import('./commands/verify-ops.js');
510
- return runPublicCommand(() => verifyCleanupCommand(run, cell, options), options.json);
511
- });
512
- verify
513
- .command('reopen')
514
- .description('Attach fresh E2B compute to retained Archil state from a test or exact fork ID')
515
- .argument('<source>', 'Run selector, or an exact archil:…:branch:… fork ID')
516
- .argument('[cell]', 'Stable test selector for a run: ordinal, riskCellId, or exact planner cellKey')
517
- .option('--side <side>', 'base or head (default head)')
518
- .option('--open-browser', 'Open the restarted application in a browser')
519
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
520
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
521
- .option('--json', 'Machine-readable attachment details')
522
- .action(async (source, cell, _options, cmd) => {
523
- const options = cmd.optsWithGlobals();
524
- const { verifyReopenCommand } = await import('./commands/verify-ops.js');
525
- return runPublicCommand(() => verifyReopenCommand(source, cell, options), options.json);
409
+ return runPublicCommand(() => verifyExploreCommand(run, caseId, { ...options, repo: options.repo }), options.json);
526
410
  });
527
- verify
528
- .command('refork')
529
- .description('Checkpoint a reopened Archil universe into a durable child fork')
530
- .argument('<source>', 'Run selector, or an exact archil:…:branch:… parent fork ID')
531
- .argument('<cell-or-child-key>', 'Stable test selector for a run, or child key for an exact fork ID')
532
- .argument('[child-key]', 'Structured stable child key when source is a run')
533
- .option('--side <side>', 'base or head (default head; only applies to a run)')
534
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
535
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
536
- .option('--json', 'Machine-readable parent, child, checkpoint, and expiry')
537
- .action(async (source, cellOrChildKey, childKey, _options, cmd) => {
538
- const options = cmd.optsWithGlobals();
539
- const { verifyReforkCommand } = await import('./commands/verify-ops.js');
540
- return runPublicCommand(() => verifyReforkCommand(source, cellOrChildKey, childKey, options), options.json);
541
- });
542
- verify
543
- .command('shell')
544
- .description('Terminal into a failing test\'s live universe (e2b sandbox connect)')
545
- .argument('<run>', verifyRunArg)
546
- .argument('<cell>', 'Stable test selector: ordinal, riskCellId, or exact planner cellKey')
547
- .option('--side <side>', 'base or head (default head; only failing tests keep a universe alive)')
548
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
549
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
550
- .action(async (run, cell, _options, cmd) => {
551
- const options = cmd.optsWithGlobals();
552
- const { verifyShellCommand } = await import('./commands/verify-ops.js');
553
- return runPublicCommand(() => verifyShellCommand(run, cell, options));
554
- });
555
- verify
556
- .command('show')
557
- .description('One test in full: finding, changed readings, request timeline, SDK log lines')
558
- .argument('<run>', verifyRunArg)
559
- .argument('<cell>', 'Test ordinal, cell id, planner cell key, or title substring')
560
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
561
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
562
- .option('--no-timeline', 'Skip the request timeline')
563
- .option('--json', 'Machine-readable detail')
564
- .action(async (run, cell, _options, cmd) => {
565
- const options = cmd.optsWithGlobals();
566
- const { verifyShowCommand } = await import('./commands/verify-ops.js');
567
- return runPublicCommand(() => verifyShowCommand(run, cell, options), options.json);
568
- });
569
- verify
570
- .command('artifacts')
571
- .description('Pull a run\'s screenshots, DOM snapshots and videos to a local directory')
572
- .argument('<run>', verifyRunArg)
573
- .option('--out <dir>', 'Destination (default: ./verify-artifacts/<runId>)')
574
- .option('--cell <selector>', 'Only artifacts for one test (ordinal, cell id, or title substring)')
575
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
576
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
577
- .option('--json', 'Machine-readable result')
578
- .action(async (run, _options, cmd) => {
579
- const options = cmd.optsWithGlobals();
580
- const { verifyArtifactsCommand } = await import('./commands/verify-ops.js');
581
- 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);
582
448
  });
583
- verify
584
- .command('sandboxes')
585
- .description('E2B sandbox inventory with run attribution; --kill-stale reclaims superseded ones')
586
- .option('--kill-stale', 'Kill verifier-attributed sandboxes superseded by newer runs (never unattributed ones)')
587
- .option('--kill-run <runId>', 'Kill every sandbox attributed to one run')
588
- .option('--server <url>', 'Control-plane URL, preferred over direct E2B access (default: http://127.0.0.1:3000)')
589
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
590
- .option('--json', 'Machine-readable inventory')
591
- .action(async (_options, cmd) => {
592
- const options = cmd.optsWithGlobals();
593
- const { verifySandboxesCommand } = await import('./commands/verify-sandboxes.js');
594
- 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);
595
472
  });
596
- verify
597
- .command('reseal')
598
- .description('Rebuild a sealed specimen bundle (guarded) and restart the control plane onto it')
599
- .argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
600
- .option('--source <dir>', 'Specimen source directory holding reseal.sh')
601
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
602
- .option('--repo-root <path>', 'Repository holding demo/cloud-verifier (default: walk up from cwd)')
603
- .option('--no-restart', 'Only rebuild the bundle; leave the running server on the old checkout')
604
- .action(async (specimen, _options, cmd) => {
605
- const options = cmd.optsWithGlobals();
606
- const { verifyResealCommand } = await import('./commands/verify-reseal.js');
607
- 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);
608
487
  });
609
- verify
610
- .command('mcp')
611
- .description('Run a stdio MCP server exposing the verifier to agents (verify_start/verify_wait/…)')
612
- .option('--server <url>', 'Control-plane URL (default: http://127.0.0.1:3000)')
613
- .option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
614
- .option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
615
- .action(async (_options, cmd) => {
616
- const options = cmd.optsWithGlobals();
617
- try {
618
- // Lazy import, same reasoning as the top-level `mcp` command: only this
619
- // path needs @modelcontextprotocol/sdk.
620
- const { runVerifyMcpServer } = await import('./commands/verify-mcp.js');
621
- await runVerifyMcpServer(options);
622
- }
623
- catch (err) {
624
- console.error(chalk.red('verify mcp server failed:'), err instanceof Error ? err.message : err);
625
- process.exit(1);
626
- }
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);
627
507
  });
628
508
  program
629
509
  .command('login')
@@ -1285,12 +1165,14 @@ hooks
1285
1165
  .command('install-session')
1286
1166
  .description('Install session-start and Stop hooks for coding CLIs')
1287
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')
1288
1169
  .addHelpText('after', `
1289
1170
  Configures your coding CLI to run \`haystack triage --hook\` on session start.
1290
1171
  This shows pending PR analysis results when you open a new terminal session.
1291
1172
  Claude Code also runs \`haystack verify precompute --hook\` when a session stops.
1292
1173
 
1293
- 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)
1294
1176
  Codex CLI: AGENTS.md instructions
1295
1177
  Gemini CLI: GEMINI.md instructions
1296
1178
 
@@ -1548,4 +1430,10 @@ program
1548
1430
  if (process.argv.length === 2) {
1549
1431
  program.help();
1550
1432
  }
1551
- 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
+ }