@haystackeditor/cli 0.25.0 → 0.26.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 (113) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/adapters/django.js +7 -0
  3. package/dist/capture/adapters/index.js +10 -0
  4. package/dist/capture/adapters/next-build.js +113 -0
  5. package/dist/capture/adapters/next.js +272 -105
  6. package/dist/capture/adapters/nuxt.js +7 -0
  7. package/dist/capture/adapters/rails.js +7 -0
  8. package/dist/capture/adapters/react-router.js +7 -0
  9. package/dist/capture/adapters/sveltekit.js +7 -0
  10. package/dist/capture/adapters/vite.js +14 -0
  11. package/dist/capture/app-config.js +27 -2
  12. package/dist/capture/csp.js +7 -6
  13. package/dist/capture/url-rewrites.js +232 -0
  14. package/dist/capture-step.js +3 -0
  15. package/dist/commands/capture-brief.js +41 -32
  16. package/dist/commands/capture-manifest.js +25 -17
  17. package/dist/commands/feedback.js +66 -0
  18. package/dist/commands/init-capture.js +44 -27
  19. package/dist/commands/init-telemetry.js +77 -14
  20. package/dist/commands/init.js +19 -4
  21. package/dist/commands/lockfile-pin.js +307 -0
  22. package/dist/commands/verify-onboarding.js +16 -0
  23. package/dist/commands/verify.js +70 -15
  24. package/dist/index.js +29 -923
  25. package/dist/schema.js +4 -10
  26. package/dist/utils/haystack-api.js +0 -36
  27. package/package.json +1 -5
  28. package/schemas/feedback.v1.json +13 -0
  29. package/schemas/init.v1.json +1 -1
  30. package/schemas/pre-verify.v2.json +240 -0
  31. package/schemas/verify-raw.v1.json +1132 -0
  32. package/schemas/verify.v2.json +655 -0
  33. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  34. package/dist/assets/hooks/agent-context/format.ts +0 -100
  35. package/dist/assets/hooks/agent-context/index.ts +0 -41
  36. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  37. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  38. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  39. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  40. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  41. package/dist/assets/hooks/agent-context/types.ts +0 -58
  42. package/dist/assets/hooks/llm-rules-template.md +0 -59
  43. package/dist/assets/hooks/package-lock.json +0 -598
  44. package/dist/assets/hooks/package.json +0 -12
  45. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  46. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  47. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  48. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  49. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  50. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  51. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  52. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  53. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  54. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  55. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  56. package/dist/assets/skills/map-your-system.md +0 -143
  57. package/dist/assets/skills/submit.md +0 -200
  58. package/dist/commands/ask.js +0 -20
  59. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  60. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  61. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  62. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  63. package/dist/commands/cloud-verifier-materialization.js +0 -704
  64. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  65. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  66. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  67. package/dist/commands/cloud-verifier-universe.js +0 -10178
  68. package/dist/commands/config.js +0 -549
  69. package/dist/commands/design-verify.js +0 -311
  70. package/dist/commands/dismiss.js +0 -159
  71. package/dist/commands/hooks.js +0 -226
  72. package/dist/commands/inbox.js +0 -137
  73. package/dist/commands/mcp.js +0 -201
  74. package/dist/commands/policy.js +0 -371
  75. package/dist/commands/pr-status.js +0 -207
  76. package/dist/commands/pr.js +0 -105
  77. package/dist/commands/prepare-universe-review.js +0 -1092
  78. package/dist/commands/production-source-deny-policy.js +0 -100
  79. package/dist/commands/request-review.js +0 -74
  80. package/dist/commands/review.js +0 -191
  81. package/dist/commands/rules.js +0 -98
  82. package/dist/commands/scaffold-provisional-universe.js +0 -806
  83. package/dist/commands/setup.js +0 -1170
  84. package/dist/commands/skills.js +0 -447
  85. package/dist/commands/submit.js +0 -745
  86. package/dist/commands/system-map.js +0 -228
  87. package/dist/commands/triage.js +0 -598
  88. package/dist/commands/webhooks.js +0 -241
  89. package/dist/states.js +0 -46
  90. package/dist/tools/detect.js +0 -832
  91. package/dist/triage/astra.js +0 -202
  92. package/dist/triage/prompts.js +0 -188
  93. package/dist/triage/runner.js +0 -200
  94. package/dist/triage/types.js +0 -7
  95. package/dist/utils/action-output.js +0 -26
  96. package/dist/utils/analysis-api.js +0 -416
  97. package/dist/utils/design-verifier-api.js +0 -294
  98. package/dist/utils/design-verifier-history.js +0 -79
  99. package/dist/utils/design-verifier-result.js +0 -424
  100. package/dist/utils/github-api.js +0 -324
  101. package/dist/utils/pending-state.js +0 -86
  102. package/dist/utils/pr-ref.js +0 -56
  103. package/dist/utils/prompter.js +0 -328
  104. package/schemas/action.v1.json +0 -22
  105. package/schemas/ask.v1.json +0 -40
  106. package/schemas/inbox.v1.json +0 -27
  107. package/schemas/pr-status.v1.json +0 -61
  108. package/schemas/pr.v1.json +0 -97
  109. package/schemas/pr.v3.json +0 -45
  110. package/schemas/setup.v1.json +0 -75
  111. package/schemas/submit.v1.json +0 -90
  112. package/schemas/triage.v1.json +0 -103
  113. package/schemas/triage.v2.json +0 -64
package/dist/index.js CHANGED
@@ -37,56 +37,11 @@ const authUseCommand = lazy(() => import('./commands/login.js'), 'authUseCommand
37
37
  const loginCommand = lazy(() => import('./commands/login.js'), 'loginCommand');
38
38
  const logoutCommand = lazy(() => import('./commands/login.js'), 'logoutCommand');
39
39
  const whoamiCommand = lazy(() => import('./commands/login.js'), 'whoamiCommand');
40
- const handleAgenticTool = lazy(() => import('./commands/config.js'), 'handleAgenticTool');
41
- const handleAutoMerge = lazy(() => import('./commands/config.js'), 'handleAutoMerge');
42
- const handleAutoFix = lazy(() => import('./commands/config.js'), 'handleAutoFix');
43
- const handleWaitForReviewers = lazy(() => import('./commands/config.js'), 'handleWaitForReviewers');
44
- const isAutoMergeEnabled = lazy(() => import('./commands/config.js'), 'isAutoMergeEnabled');
45
- const isAutoFixEnabled = lazy(() => import('./commands/config.js'), 'isAutoFixEnabled');
46
- const installSkills = lazy(() => import('./commands/skills.js'), 'installSkills');
47
- const listSkills = lazy(() => import('./commands/skills.js'), 'listSkills');
48
- const cloudVerifierUniverseValidateCommand = lazy(() => import('./commands/cloud-verifier-universe.js'), 'cloudVerifierUniverseValidateCommand');
49
- const cloudVerifierIdentityCensusCommand = lazy(() => import('./commands/cloud-verifier-identity-census.js'), 'cloudVerifierIdentityCensusCommand');
50
- const cloudVerifierDataStoreDriftCommand = lazy(() => import('./commands/cloud-verifier-data-store-drift.js'), 'cloudVerifierDataStoreDriftCommand');
51
- const cloudVerifierBehaviorsValidateCommand = lazy(() => import('./commands/cloud-verifier-behaviors.js'), 'cloudVerifierBehaviorsValidateCommand');
52
- const prepareUniverseReviewCommand = lazy(() => import('./commands/prepare-universe-review.js'), 'prepareUniverseReviewCommand');
53
- const scaffoldProvisionalUniverseCommand = lazy(() => import('./commands/scaffold-provisional-universe.js'), 'scaffoldProvisionalUniverseCommand');
54
- const hooksInstall = lazy(() => import('./commands/hooks.js'), 'hooksInstall');
55
- const hooksStatus = lazy(() => import('./commands/hooks.js'), 'hooksStatus');
56
- const submitCommand = lazy(() => import('./commands/submit.js'), 'submitCommand');
57
- const installSessionHooks = lazy(() => import('./commands/install-session-hooks.js'), 'installSessionHooks');
58
- const sessionHooksStatus = lazy(() => import('./commands/install-session-hooks.js'), 'sessionHooksStatus');
59
- const listPolicies = lazy(() => import('./commands/policy.js'), 'listPolicies');
60
- const addPolicy = lazy(() => import('./commands/policy.js'), 'addPolicy');
61
- const removePolicy = lazy(() => import('./commands/policy.js'), 'removePolicy');
62
- const initPolicies = lazy(() => import('./commands/policy.js'), 'initPolicies');
63
- const addInstruction = lazy(() => import('./commands/policy.js'), 'addInstruction');
64
- const triageCommand = lazy(() => import('./commands/triage.js'), 'triageCommand');
65
- const designVerifyCommand = lazy(() => import('./commands/design-verify.js'), 'designVerifyCommand');
66
- const dismissCommand = lazy(() => import('./commands/dismiss.js'), 'dismissCommand');
67
- const markReviewedCommand = lazy(() => import('./commands/dismiss.js'), 'markReviewedCommand');
68
- const undismissCommand = lazy(() => import('./commands/dismiss.js'), 'undismissCommand');
69
- const requestReviewCommand = lazy(() => import('./commands/request-review.js'), 'requestReviewCommand');
70
- const reviewCommand = lazy(() => import('./commands/review.js'), 'reviewCommand');
71
- const prStatusCommand = lazy(() => import('./commands/pr-status.js'), 'prStatusCommand');
72
- const prReadCommand = lazy(() => import('./commands/pr.js'), 'prReadCommand');
73
- const inboxListCommand = lazy(() => import('./commands/inbox.js'), 'inboxListCommand');
74
- const askHaystackCommand = lazy(() => import('./commands/ask.js'), 'askHaystackCommand');
75
40
  const telemetryInstrumentCommand = lazy(() => import('./commands/telemetry.js'), 'telemetryInstrumentCommand');
76
41
  const telemetrySettingsCommand = lazy(() => import('./commands/telemetry.js'), 'telemetrySettingsCommand');
77
42
  const telemetryTokenCommand = lazy(() => import('./commands/telemetry-token.js'), 'telemetryTokenCommand');
78
- const setupCommand = lazy(() => import('./commands/setup.js'), 'setupCommand');
79
43
  const schemaCommand = lazy(() => import('./commands/schema-cmd.js'), 'schemaCommand');
80
44
  const listSchemas = lazy(() => import('./commands/schema-cmd.js'), 'listSchemas');
81
- const registerWebhook = lazy(() => import('./commands/webhooks.js'), 'registerWebhook');
82
- const listWebhooks = lazy(() => import('./commands/webhooks.js'), 'listWebhooks');
83
- const rotateWebhookSecret = lazy(() => import('./commands/webhooks.js'), 'rotateWebhookSecret');
84
- const setWebhookEnabled = lazy(() => import('./commands/webhooks.js'), 'setWebhookEnabled');
85
- const listDeliveries = lazy(() => import('./commands/webhooks.js'), 'listDeliveries');
86
- const replayDelivery = lazy(() => import('./commands/webhooks.js'), 'replayDelivery');
87
- const editRulesCommand = lazy(() => import('./commands/rules.js'), 'editRulesCommand');
88
- const validateRulesCommand = lazy(() => import('./commands/rules.js'), 'validateRulesCommand');
89
- const systemMapValidateCommand = lazy(() => import('./commands/system-map.js'), 'systemMapValidateCommand');
90
45
  const verifyCommand = lazy(() => import('./commands/verify.js'), 'verifyCommand');
91
46
  const preVerifyCommand = lazy(() => import('./commands/verify.js'), 'preVerifyCommand');
92
47
  const headlessLoginCommand = lazy(() => import('./commands/tokens.js'), 'headlessLoginCommand');
@@ -96,12 +51,7 @@ const revokeTokenCommand = lazy(() => import('./commands/tokens.js'), 'revokeTok
96
51
  const databaseProfileCommand = lazy(() => import('./commands/db-profile.js'), 'databaseProfileCommand');
97
52
  const databaseProfileShowCommand = lazy(() => import('./commands/db-profile-show.js'), 'databaseProfileShowCommand');
98
53
  const databaseProfileUploadCommand = lazy(() => import('./commands/db-profile-upload.js'), 'databaseProfileUploadCommand');
99
- // `mcp` is imported lazily inside its command action (below), NOT at the top level.
100
- // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
101
- // the startup import graph means a missing/broken SDK can never crash core commands
102
- // like `triage`/`submit`. (0.15.12 shipped `mcp.js` but published its package.json
103
- // WITHOUT `@modelcontextprotocol/sdk`, so the eager import here hard-crashed every
104
- // command at startup with ERR_MODULE_NOT_FOUND.)
54
+ const feedbackCommand = lazy(() => import('./commands/feedback.js'), 'feedbackCommand');
105
55
  async function runPublicCommand(action, json) {
106
56
  try {
107
57
  await action();
@@ -154,6 +104,7 @@ program
154
104
  .option('--app <dir>', 'The app whose telemetry init sets up, when the repository has several; see below')
155
105
  .option('--origin <url>', 'A production origin of the app (repeat for several): browser capture accepts events only from these', (value, previous = []) => [...previous, value])
156
106
  .option('--consent <choice>', 'How the site asks visitors for consent: its consent platform, or not-required when your user decided so')
107
+ .option('--url-rewrites <answer>', 'When init asks: none, or hidden-segment:<name> when the app\'s middleware adds a leading [<name>] segment to page paths')
157
108
  .option('--json', 'The result as one JSON document on stdout (see `haystack schema init`)')
158
109
  .addHelpText('after', `
159
110
  There is no setup file: Haystack works out how to run the app from what the
@@ -219,48 +170,6 @@ Examples:
219
170
  haystack init --yes --json --notes haystack-notes.json
220
171
  `)
221
172
  .action(options => runPublicCommand(() => initCommand(options), options.json));
222
- program
223
- .command('setup')
224
- .description('Onboarding wizard — scan repos for rules/policies and write Haystack config')
225
- .option('--json', 'Agent mode: emit NDJSON events on stdout, read answers on stdin (no TTY prompts)')
226
- .option('--repo <repo...>', 'Repo(s) to configure (owner/name); skips the repo picker')
227
- .option('-y, --yes', 'Accept defaults non-interactively (keep all discovered items, confirm write, ENABLE auto-merge — pass --no-auto-merge to opt out)')
228
- // Only the negative form exists; there is no --auto-merge. Without the flag the
229
- // wizard asks (detected via getOptionValueSource below).
230
- .option('--no-auto-merge', 'Turn auto-merge off in the written config (omit to be asked)')
231
- .option('--answers <file>', 'JSON file of {questionId: value} pre-supplied answers')
232
- .addHelpText('after', `
233
- Steps: verify GitHub App → select repos → scan (rules/policies/instructions) →
234
- review → write .haystack/pr-rules.yml + review-policy.md + .haystack.json.
235
-
236
- Three ways to run:
237
- • Interactive (default): prompts in your terminal.
238
- • Autonomous/CI: supply every decision via flags, e.g.
239
- haystack setup --repo owner/name --yes --no-auto-merge
240
- • Coding agent: --json speaks NDJSON on stdout and reads answers on stdin, so an
241
- agent can auto-answer or relay questions to the user. Pre-supplied answers
242
- (flags / --answers) skip the matching question.
243
-
244
- Requires authentication — run \`haystack login\` first.
245
-
246
- Examples:
247
- haystack setup
248
- haystack setup --repo owner/name --yes
249
- haystack setup --json --repo owner/name
250
- `)
251
- .action((options, command) => {
252
- // auto-merge: false when --no-auto-merge was passed, otherwise undefined (ask).
253
- // commander defaults autoMerge to true because of the --no- form, so check
254
- // the value source rather than the value.
255
- const autoMergeExplicit = command.getOptionValueSource('autoMerge') === 'cli';
256
- return setupCommand({
257
- json: options.json,
258
- repos: options.repo,
259
- yes: options.yes,
260
- autoMerge: autoMergeExplicit ? options.autoMerge : undefined,
261
- answersFile: options.answers,
262
- });
263
- });
264
173
  program
265
174
  .command('status')
266
175
  .description('Check if .haystack.json exists and is valid')
@@ -276,7 +185,8 @@ const verify = program
276
185
  .option('--intent <sentence>', 'What you were asked to do, in the user\'s words (the task, not what your code does): the judge checks the app against it')
277
186
  .option('--idea <text>', 'Something to try in the app, said as you would to a tester (repeat for more); each is explored first', (value, previous = []) => [...previous, value])
278
187
  .option('--ideas-file <path>', 'A JSON array of more ideas')
279
- .option('--json', 'The crawl as one JSON document on stdout (see `haystack schema verify`)')
188
+ .option('--json', 'The report as one JSON document on stdout (see `haystack schema verify`)')
189
+ .option('--raw', 'The service\'s whole crawl record as one JSON document, for scripts (large; see `haystack schema verify-raw`)')
280
190
  .addHelpText('after', `
281
191
  Run inside a git checkout; nothing needs to be committed or pushed. It captures
282
192
  the checkout (committed and uncommitted changes) exactly as the stop hook does,
@@ -332,7 +242,7 @@ Examples:
332
242
  haystack verify history owner/repo --limit 20
333
243
  haystack verify hosted status cv_<48 lowercase hex characters> --wait
334
244
  `)
335
- .action(options => runPublicCommand(() => verifyCommand(options), options.json));
245
+ .action(options => runPublicCommand(() => verifyCommand(options), options.json || options.raw));
336
246
  program
337
247
  .command('pre-verify')
338
248
  .description('Before a check: what your change touches, and what could break in it')
@@ -346,9 +256,10 @@ same way, builds and prepares the change when nothing has yet (the turn-end
346
256
  hook usually has), and prints its brief: the functions the change touches, the
347
257
  changed ones and those one step away first (every one is in --json), and
348
258
  Haystack's own ideas of what could break, each with what to set up, what to do
349
- and what to watch. With capture set up (.haystack/capture.json), it also shows
350
- the routes the change touches with their share of real users' sessions. It
351
- never starts a crawl.
259
+ and what to watch. With capture set up (an app's .haystack/capture.json, such
260
+ as apps/web/.haystack/capture.json), it also shows, per app, the routes the
261
+ change touches with their share of real users' sessions. It never starts a
262
+ crawl.
352
263
 
353
264
  Then hand back what you were asked to do and what to try:
354
265
  haystack verify --intent "<what you were asked to do, in the user's words>" --idea "<something to try>"
@@ -454,7 +365,7 @@ verify
454
365
  process.exitCode = 0;
455
366
  });
456
367
  const hostedVerify = verify
457
- .command('hosted')
368
+ .command('hosted', { hidden: true })
458
369
  .description('Run the production Cloud Verifier against exact GitHub commits');
459
370
  hostedVerify
460
371
  .command('start')
@@ -580,7 +491,7 @@ verify
580
491
  return runPublicCommand(() => verifyExploreCommand(run, caseId, { ...options, repo: options.repo }), options.json);
581
492
  });
582
493
  const caseBatch = program
583
- .command('case-batch')
494
+ .command('case-batch', { hidden: true })
584
495
  .description('Submit and poll product-fuzzing case batches on the hosted coordinator (CASE-BATCH-V1)');
585
496
  caseBatch
586
497
  .command('submit')
@@ -711,6 +622,22 @@ just those paths with --only.
711
622
  const { caseBatchBundleCommand } = await import('./commands/case-batch.js');
712
623
  return runPublicCommand(() => caseBatchBundleCommand(runId, options), options.json);
713
624
  });
625
+ program
626
+ .command('feedback <message...>')
627
+ .description('Tell the Haystack team something went wrong or got in your way (- reads the message from stdin)')
628
+ .option('--run <id>', 'The verification run it is about, when there is one')
629
+ .option('--account <login>', 'Use a specific saved Haystack account')
630
+ .option('--json', 'The result as one JSON document on stdout (see `haystack schema feedback`)')
631
+ .addHelpText('after', `
632
+ For coding agents as much as people: when Haystack fails, confuses you or is
633
+ missing something, say so in a sentence and keep going. The team sees it at
634
+ once, with the CLI version, the platform and this checkout's repository.
635
+
636
+ Examples:
637
+ haystack feedback "verify said the app never started, but it was up on :3000"
638
+ haystack feedback --run cv_<id> "the bug it reported is the intended change"
639
+ `)
640
+ .action((words, options) => runPublicCommand(() => feedbackCommand(words, options), options.json));
714
641
  program
715
642
  .command('login')
716
643
  .description('Add or refresh a saved GitHub account (use --headless for CI tokens)')
@@ -799,171 +726,6 @@ authProgram
799
726
  .command('use <login>')
800
727
  .description('Set the active Haystack GitHub account')
801
728
  .action(authUseCommand);
802
- program
803
- .command('submit')
804
- .description('Create a PR from current changes')
805
- .option('--account <login>', 'Saved Haystack account login to use for GitHub API calls')
806
- .option('--title <title>', 'PR title (default: last commit message)')
807
- .option('--body <body>', 'PR body (default: commit messages since base)')
808
- .option('--body-file <path>', 'Read PR body from file (use "-" for stdin)')
809
- .option('--base <branch>', 'Base branch (default: main or master)')
810
- .option('--draft', 'Create as draft PR')
811
- .option('--review [reviewer]', 'Request human review (BLOCKS auto-merge — only use when explicitly asked)')
812
- .option('--force', 'Skip pre-PR triage checks')
813
- .addOption(new Option('--auto-fix', 'Alpha auto-fix for entitled repositories').hideHelp())
814
- .option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
815
- .option('--no-auto-merge', 'Do not apply the auto-merge label')
816
- .option('--no-wait', 'Skip waiting for analysis results')
817
- .option('--json', 'Machine-readable: one JSON document on stdout (see `haystack schema submit`), progress on stderr')
818
- .option('--triage-timeout <seconds>', 'Abort a triage checker after this many seconds with no data from the model (overrides .haystack.json triage.timeoutMs)', (v) => parseInt(v, 10))
819
- .addHelpText('after', `
820
- This command is designed for AI coding agents to submit PRs.
821
-
822
- 1. Runs pre-PR triage (code review and rules) on gpt-6-astra
823
- 2. Pushes the current branch to origin
824
- 3. Creates a pull request on GitHub
825
- 4. Waits for Haystack analysis results (triggered via GitHub App webhook)
826
-
827
- Pre-PR Triage:
828
- Before creating the PR, haystack sends the diff to gpt-6-astra (OpenAI
829
- Responses API, reasoning effort "xhigh", structured output) in parallel for:
830
- • Code review bugs (logic errors, null crashes, security issues, secrets)
831
- • Rule violations (from .haystack/pr-rules.yml and CLAUDE.md/AGENTS.md etc.)
832
-
833
- Uses OPENAI_API_KEY if set, else the SSM parameter
834
- /haystack/secrets/shared/prod/OPENAI_API_KEY (us-west-2) via the default
835
- AWS credential chain. If a checker call fails, submit prints
836
- "<checker> failed: <error>" and continues; only findings with severity
837
- "error" block the PR.
838
-
839
- Use --force to skip triage entirely.
840
- Use --no-wait to skip waiting for analysis results.
841
-
842
- • --triage-timeout <sec> Abort a checker when the model stream has been
843
- silent this long (default: 300s). There is no
844
- total-time cap.
845
-
846
- Persist it in .haystack.json to apply per project:
847
-
848
- {
849
- "triage": {
850
- "timeoutMs": 300000
851
- }
852
- }
853
-
854
- Review Routing:
855
- If repo auto-merge is enabled in .haystack.json, plain \`haystack submit\`
856
- enters the auto-merge queue after submission. Otherwise it still runs the
857
- same Haystack analysis workflow without enabling auto-merge.
858
-
859
- ⚠ Only use --review when the user EXPLICITLY asks for human review.
860
- --review BLOCKS auto-merge — the PR will NOT merge until a human approves,
861
- which delays merging. Do not add --review "just to be safe".
862
-
863
- • --review Labels the PR "haystack:needs-review" for the
864
- needs-assignment queue (a teammate will pick it up)
865
- • --review <username> Same label, plus requests review from that GitHub user
866
-
867
- Machine-readable output (--json):
868
- stdout carries exactly one JSON document (schema: \`haystack schema submit\`);
869
- all progress goes to stderr. On success the payload includes the PR ref,
870
- the resolved title/body and their sources, auto-merge/auto-fix state with
871
- source, and the analysis outcome. On failure it is an error envelope
872
- { status: "error", error: "..." }.
873
-
874
- Exit codes:
875
- 0 PR created/updated (including verdict "needs_input" — the PR exists;
876
- read the verdict from the JSON or from \`haystack triage\`)
877
- 1 Hard failure (bad flags, auth, push, PR creation, blocking triage
878
- findings, or the analysis status check itself failing)
879
-
880
- Examples:
881
- haystack submit # Recommended: triage, create PR, auto-merge queue
882
- haystack submit --json # Agent mode: JSON on stdout, progress on stderr
883
- haystack submit --force # Skip triage
884
- haystack submit --no-wait # Don't wait for analysis
885
- haystack submit --title "Fix auth" # Custom PR title
886
- haystack submit --body "## Summary" # Custom PR body (inline)
887
- haystack submit --body-file body.md # Read body from file (supports markdown)
888
- echo "## Summary" | haystack submit --body-file - # Read body from stdin
889
- haystack submit --base develop # Target a different base branch
890
- haystack submit --draft # Create as draft PR
891
- haystack submit --review # ⚠ Blocks auto-merge, needs human approval
892
- haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
893
- haystack submit --account octocat # Use a specific saved Haystack account for this submit
894
- haystack submit --triage-timeout 600 # Allow 10 minutes of model silence per checker
895
- `)
896
- .action(async (options, command) => {
897
- // Resolve --auto-merge / --auto-fix defaults from .haystack.json when not
898
- // explicitly set, and record WHERE each value came from so submit can
899
- // announce the source (silent config-driven defaults are invisible to
900
- // the caller otherwise).
901
- const autoMergeFromCli = command.getOptionValueSource('autoMerge') === 'cli';
902
- const autoFixFromCli = command.getOptionValueSource('autoFix') === 'cli';
903
- if (!autoMergeFromCli) {
904
- options.autoMerge = await isAutoMergeEnabled();
905
- }
906
- // Auto-fix remains alpha — the explicit --auto-fix flag still surfaces
907
- // discouragement warnings; opting in via repo config is the supported path.
908
- if (!autoFixFromCli) {
909
- options.autoFix = await isAutoFixEnabled();
910
- }
911
- options.autoMergeSource = autoMergeFromCli ? 'flag' : 'config';
912
- options.autoFixSource = autoFixFromCli ? 'flag' : 'config';
913
- return submitCommand(options);
914
- });
915
- program
916
- .command('triage [pr]')
917
- .description('View Haystack analysis results for a PR')
918
- .option('--json', 'Output as JSON')
919
- .option('--hook', 'Minimal single-line output for session-start hooks')
920
- .option('--clear', 'Clear pending submit state')
921
- .option('--no-wait', 'Exit immediately if analysis is still pending')
922
- .addHelpText('after', `
923
- Look up triage results (bugs, rule violations, verdict) for any PR.
924
- When called without a PR identifier, checks the last submitted PR.
925
-
926
- Runs automatically on CLI session start if session hooks are installed.
927
-
928
- PR identifier formats:
929
- (none) Last submitted PR
930
- 123 PR number (uses current repo)
931
- #123 PR number with hash
932
- owner/repo#123 Fully qualified
933
- https://github.com/owner/repo/pull/123 GitHub URL
934
-
935
- By default, if analysis is still in progress, the command will poll
936
- for up to 5 minutes. Use --no-wait to exit immediately instead.
937
-
938
- Examples:
939
- haystack triage # Last submitted PR
940
- haystack triage 42 # Current repo, PR #42
941
- haystack triage acme/widgets#99 # Specific repo
942
- haystack triage https://github.com/o/r/pull/1 # From URL
943
- haystack triage 42 --json # Machine-readable output
944
- haystack triage 42 --no-wait # Don't wait if pending
945
- haystack triage --hook # Session-start hook output
946
- haystack triage --clear # Clear pending state
947
- `)
948
- .action(triageCommand);
949
- const inbox = program.command('inbox').description('List Haystack work needing your attention');
950
- inbox
951
- .command('list')
952
- .description('List PRs in your Haystack inbox')
953
- .option('--json', 'Output as JSON')
954
- .action((options) => runPublicCommand(() => inboxListCommand(options), options.json));
955
- const prProgram = program.command('pr').description('Inspect one pull request');
956
- prProgram
957
- .command('get <ref>')
958
- .description('Get triage and merge blockers')
959
- .option('--json', 'Output as JSON')
960
- .action((ref, options) => runPublicCommand(() => prReadCommand(ref, options), options.json));
961
- program
962
- .command('ask <ref> <question>')
963
- .description('Ask Haystack about a pull request')
964
- .option('--json', 'Output the answer and every consulted customer-facing source as JSON')
965
- .option('--session <id>', 'Continue a previous Ask Haystack session')
966
- .action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options), options.json));
967
729
  const telemetry = program.command('telemetry').description('Add privacy-safe production telemetry without an application SDK')
968
730
  .addHelpText('after', `
969
731
  Next.js bundles its server code, so instead of \`instrument\` add the CLI as a
@@ -1045,6 +807,7 @@ program.command('capture').description('Real users\' journeys and devices: the t
1045
807
  .description('Publish the app\'s route manifest into its static output (the build step `haystack init` adds)')
1046
808
  .option('--app <dir>', 'The app directory (default: the current directory)')
1047
809
  .option('--rails-routes <file>', 'Rails: the routes file the rake task wrote')
810
+ .option('--adapter <id>', 'The framework adapter to use instead of the one init recorded in .haystack/capture.json')
1048
811
  .option('--json', 'The release, counts and files written as JSON')
1049
812
  .action((options) => runPublicCommand(() => captureManifestCommand(options), options.json));
1050
813
  // ─── db ──────────────────────────────────────────────────────────────────────
@@ -1096,583 +859,6 @@ start prints why no stand-in was used and names the declared ids; upload again w
1096
859
  that dependency; when both apply, the newer upload is used.
1097
860
  `)
1098
861
  .action((file, options) => runPublicCommand(() => databaseProfileUploadCommand(file, options)));
1099
- program
1100
- .command('dismiss <pr>')
1101
- .description('Dismiss analysis findings for a PR')
1102
- .option('--json', 'Machine-readable output (see `haystack schema action`)')
1103
- .addHelpText('after', `
1104
- Dismiss analysis findings for a PR, moving it from "Issues Found" to
1105
- "Good to Merge" in the Haystack feed.
1106
-
1107
- The override is tied to the PR's current HEAD commit. If a new commit is
1108
- pushed, the override is invalidated and you'll need to dismiss again.
1109
-
1110
- PR identifier formats:
1111
- 123 PR number (uses current repo)
1112
- #123 PR number with hash
1113
- owner/repo#123 Fully qualified
1114
- https://github.com/owner/repo/pull/123 GitHub URL
1115
-
1116
- Examples:
1117
- haystack dismiss 42 # Dismiss findings for PR #42
1118
- haystack dismiss acme/widgets#99 # Dismiss for specific repo
1119
- `)
1120
- .action(dismissCommand);
1121
- program
1122
- .command('design-verify <pr>')
1123
- // "verification" is a banned word in top-level help (public-contract test
1124
- // keeps retired auto-fix/verification wording out of the CLI surface).
1125
- .description('Run a blind design-by-execution review of a PR')
1126
- .option('--json', 'Machine-readable output')
1127
- .option('--history', 'Show recent execution reliability for this repository without starting a run')
1128
- .option('--limit <attempts>', 'Maximum recorded terminal attempts to show with --history (1–100, default 20)')
1129
- .option('--no-wait', 'Return after the run is admitted instead of waiting for its verdict')
1130
- .option('--run <run-id>', 'Resume and wait for an existing exact run id')
1131
- .option('--poll-interval <seconds>', 'Status polling interval', '3')
1132
- .option('--timeout <seconds>', 'Maximum time to wait; the server run continues after timeout', '7200')
1133
- .option('--case <case-id>', 'Select one exact generated case for universe exploration')
1134
- .option('--universe <role>', 'Select control-before, before, or after')
1135
- .option('--replay', 'Replay the registered case command in the selected universe')
1136
- .option('--exec-json <argv>', 'Run a JSON argv array in the selected universe')
1137
- .option('--cwd <path>', 'Repository-relative working directory for --exec-json')
1138
- .option('--command-timeout <seconds>', 'Maximum selected-universe command runtime')
1139
- .option('--action <action-id>', 'Resume one exact universe action')
1140
- .addHelpText('after', `
1141
- Executes the change instead of reading it: infers intent from the diff
1142
- alone, designs pre-registered behavioral test cases, runs control-before,
1143
- before, and after in isolated sandboxes, and reports anomaly candidates
1144
- outside the registered intent.
1145
-
1146
- The command waits for a behavioral verdict by default. Use --no-wait to
1147
- return the run id immediately, or --run <run-id> to resume waiting.
1148
-
1149
- Read recent execution reliability across this repository's PRs:
1150
- haystack design-verify owner/repo#123 --history --limit 20
1151
- History covers recorded terminal attempts, including partial or failed runs;
1152
- it does not measure bug-detection accuracy or whether changes are bug-free.
1153
-
1154
- After a completed run, inspect or replay one retained case universe without
1155
- receiving provider credentials:
1156
- haystack design-verify owner/repo#123 --run RUN --case case-00001 --universe before --replay
1157
- haystack design-verify owner/repo#123 --run RUN --case case-00001 --universe after --exec-json '["rg","TODO"]'
1158
-
1159
- PR identifier formats:
1160
- 123 PR number (uses current repo)
1161
- owner/repo#123 Fully qualified
1162
- https://github.com/owner/repo/pull/123 GitHub URL
1163
- `)
1164
- .action(designVerifyCommand);
1165
- program
1166
- .command('mark-reviewed <pr>')
1167
- .description('Mark human review as not needed for a PR')
1168
- .option('--json', 'Machine-readable output (see `haystack schema action`)')
1169
- .addHelpText('after', `
1170
- Mark human review as not needed for a PR, moving it from "Needs Review"
1171
- to "Good to Merge" in the Haystack feed.
1172
-
1173
- The override is tied to the PR's current HEAD commit. If a new commit is
1174
- pushed, the override is invalidated and you'll need to mark it again.
1175
-
1176
- PR identifier formats:
1177
- 123 PR number (uses current repo)
1178
- #123 PR number with hash
1179
- owner/repo#123 Fully qualified
1180
- https://github.com/owner/repo/pull/123 GitHub URL
1181
-
1182
- Examples:
1183
- haystack mark-reviewed 42 # Mark review not needed for PR #42
1184
- haystack mark-reviewed acme/widgets#99 # Mark for specific repo
1185
- `)
1186
- .action(markReviewedCommand);
1187
- program
1188
- .command('undismiss <pr>')
1189
- .description('Undo a dismiss or mark-reviewed override for a PR')
1190
- .option('--json', 'Machine-readable output (see `haystack schema action`)')
1191
- .addHelpText('after', `
1192
- Clear all overrides (dismissed findings and/or review-not-needed) for a PR,
1193
- returning it to its original feed bucket.
1194
-
1195
- PR identifier formats:
1196
- 123 PR number (uses current repo)
1197
- #123 PR number with hash
1198
- owner/repo#123 Fully qualified
1199
- https://github.com/owner/repo/pull/123 GitHub URL
1200
-
1201
- Examples:
1202
- haystack undismiss 42 # Undo overrides for PR #42
1203
- haystack undismiss acme/widgets#99 # Undo for specific repo
1204
- `)
1205
- .action(undismissCommand);
1206
- program
1207
- .command('review [pr]')
1208
- .description('Trigger a fresh Haystack analysis for a PR (machine analysis — for HUMAN review use request-review)')
1209
- .option('--no-wait', 'Exit after triggering instead of waiting for results')
1210
- .option('--json', 'Machine-readable output (see `haystack schema action`)')
1211
- .addHelpText('after', `
1212
- Re-run full Haystack analysis on a PR's current head.
1213
-
1214
- Haystack analyzes a PR once when it's opened (or marked ready); later
1215
- pushes only get a resolution check against the existing findings. Use
1216
- this command when new commits deserve fresh detection, or any time you
1217
- want a second opinion.
1218
-
1219
- When called without a PR identifier, uses the last submitted PR.
1220
-
1221
- PR identifier formats:
1222
- (none) Last submitted PR
1223
- 123 PR number (uses current repo)
1224
- #123 PR number with hash
1225
- owner/repo#123 Fully qualified
1226
- https://github.com/owner/repo/pull/123 GitHub URL
1227
-
1228
- Examples:
1229
- haystack review # Last submitted PR
1230
- haystack review 42 # Current repo, PR #42
1231
- haystack review acme/widgets#99 # Specific repo
1232
- haystack review 42 --no-wait # Trigger and exit
1233
- `)
1234
- .action(reviewCommand);
1235
- program
1236
- .command('request-review <pr> [reviewer]')
1237
- .description('Tag a PR as needing human review')
1238
- .option('--json', 'Machine-readable output (see `haystack schema action`)')
1239
- .addHelpText('after', `
1240
- Tag any PR with "needs human review", even after it was created.
1241
- Adds the haystack:needs-review label to the PR, moving it from
1242
- "Good to Merge" to the "Needs Assignment" queue in the feed.
1243
-
1244
- Optionally specify a reviewer to request review from a specific
1245
- GitHub user.
1246
-
1247
- PR identifier formats:
1248
- 123 PR number (uses current repo)
1249
- #123 PR number with hash
1250
- owner/repo#123 Fully qualified
1251
- https://github.com/owner/repo/pull/123 GitHub URL
1252
-
1253
- Examples:
1254
- haystack request-review 42 # Tag PR #42 for human review
1255
- haystack request-review 42 octocat # Tag and request review from octocat
1256
- haystack request-review acme/widgets#99 # Specific repo
1257
- `)
1258
- .action(requestReviewCommand);
1259
- program
1260
- .command('pr-status <pr>')
1261
- .description('Show the current Haystack status of a PR')
1262
- .option('--json', 'Output as JSON')
1263
- .addHelpText('after', `
1264
- Show what bucket a PR is in within the Haystack pipeline:
1265
- analyzing, good-to-merge, issues, needs-assignment, etc.
1266
-
1267
- PR identifier formats:
1268
- 123 PR number (uses current repo)
1269
- #123 PR number with hash
1270
- owner/repo#123 Fully qualified
1271
- https://github.com/owner/repo/pull/123 GitHub URL
1272
-
1273
- Examples:
1274
- haystack pr-status 42 # Current repo, PR #42
1275
- haystack pr-status acme/widgets#99 # Specific repo
1276
- haystack pr-status https://github.com/o/r/pull/1 # From URL
1277
- haystack pr-status 42 --json # Machine-readable output
1278
- `)
1279
- .action(prStatusCommand);
1280
- // Config subcommands
1281
- const config = program
1282
- .command('config')
1283
- .description('Manage user preferences');
1284
- config
1285
- .command('agentic-tool [tool]')
1286
- .description('Set agentic tool (opencode|claude-code|codex|status)')
1287
- .addHelpText('after', `
1288
- Tools:
1289
- opencode OpenCode (Haystack billing) - default
1290
- claude-code Claude Code (your Claude Max subscription)
1291
- codex Codex CLI (your ChatGPT subscription)
1292
- status Show current setting (default)
1293
-
1294
- This sets your account-level default. Projects can override
1295
- this in .haystack.json under agentic.tool.
1296
-
1297
- Examples:
1298
- haystack config agentic-tool # Show current setting
1299
- haystack config agentic-tool opencode # Use Haystack billing
1300
- haystack config agentic-tool claude-code # Use your Claude Max
1301
- haystack config agentic-tool codex # Use your ChatGPT
1302
- `)
1303
- .action(handleAgenticTool);
1304
- config
1305
- .command('auto-merge [action]')
1306
- .description('Auto-merge safe PRs submitted via haystack submit (on|off|status)')
1307
- .addHelpText('after', `
1308
- Actions:
1309
- on, enable, true Enable auto-merge for safe PRs
1310
- off, disable, false Disable auto-merge
1311
- status Show current status (default)
1312
-
1313
- When enabled, PRs submitted via \`haystack submit\` will be
1314
- automatically merged if Haystack analysis finds no issues
1315
- (safe to merge). PRs with bugs or rule violations still
1316
- require manual review.
1317
-
1318
- Examples:
1319
- haystack config auto-merge # Show current status
1320
- haystack config auto-merge on # Enable auto-merge
1321
- haystack config auto-merge off # Disable auto-merge
1322
- `)
1323
- .action(handleAutoMerge);
1324
- config
1325
- .command('auto-fix [action]', { hidden: true })
1326
- .description('(Alpha) Auto-fix mechanical issues on PRs submitted via haystack submit (on|off|status)')
1327
- .addHelpText('after', `
1328
- Actions:
1329
- on, enable, true Enable auto-fix for mechanical issues
1330
- off, disable, false Disable auto-fix
1331
- status Show current status (default)
1332
-
1333
- ⚠ Auto-fix is alpha. When enabled, PRs submitted via \`haystack submit\`
1334
- are labeled haystack:auto-fix. The Haystack analysis pipeline then
1335
- dispatches the sandbox fixer for issues classified as straightforward
1336
- and mechanical. Judgment calls, policy violations, and weak coverage
1337
- findings still surface in the Feed for human review.
1338
-
1339
- Examples:
1340
- haystack config auto-fix # Show current status
1341
- haystack config auto-fix on # Enable auto-fix (alpha)
1342
- haystack config auto-fix off # Disable auto-fix
1343
- `)
1344
- .action(handleAutoFix);
1345
- config
1346
- .command('wait-for-reviewers [action] [reviewers...]')
1347
- .description('Configure which AI reviewers the merge queue waits for')
1348
- .addHelpText('after', `
1349
- Actions:
1350
- list Show configured reviewer bots (default)
1351
- add <names> Add one or more reviewer bots
1352
- remove <names> Remove one or more reviewer bots
1353
- clear Remove all reviewer bots
1354
-
1355
- Accepts friendly names (cursor, coderabbit) or exact
1356
- GitHub bot usernames (cursor-bugbot[bot]).
1357
-
1358
- Stored in .haystack.json under merge_queue.wait_for_reviewer.bots.
1359
- The merge queue will block merging until ALL configured
1360
- bots have posted a review or comment on the PR.
1361
-
1362
- Examples:
1363
- haystack config wait-for-reviewers # Show status
1364
- haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
1365
- haystack config wait-for-reviewers add cursor coderabbit # Add multiple
1366
- haystack config wait-for-reviewers add cursor-bugbot[bot] # Raw bot username
1367
- haystack config wait-for-reviewers remove cursor # Stop waiting
1368
- haystack config wait-for-reviewers clear # Wait for none
1369
- `)
1370
- .action(handleWaitForReviewers);
1371
- // Skills subcommands
1372
- const skills = program
1373
- .command('skills')
1374
- .description('Manage AI skills for coding agents');
1375
- skills
1376
- .command('install')
1377
- .description('Install portable Haystack skills and optional coding-CLI shims')
1378
- .option('--cli <name>', 'Target CLI: claude, codex, cursor, or manual')
1379
- .addHelpText('after', `
1380
- This installs portable skills in the Git repository's .agents/skills directory:
1381
- /submit - Submit a PR via Haystack
1382
- /map-your-system - Map how Haystack QA can run this system
1383
- /map-cloud-verifier-universe - Map a production-derived hermetic universe
1384
- Supported CLIs:
1385
- claude Also install Claude Code command shims
1386
- codex Use portable .agents/skills discovery only
1387
- cursor Use portable .agents/skills discovery only
1388
- manual Install portable skills and show their location
1389
-
1390
- Examples:
1391
- haystack skills install # Install portable skills only
1392
- haystack skills install --cli codex # Install for Codex only
1393
- haystack skills install --cli claude # Also install Claude command shims
1394
- `)
1395
- .action(async (opts) => {
1396
- try {
1397
- await installSkills(opts);
1398
- }
1399
- catch (err) {
1400
- console.error(chalk.red('skills install failed:'), err instanceof Error ? err.message : err);
1401
- process.exit(1);
1402
- }
1403
- });
1404
- skills
1405
- .command('list')
1406
- .description('List available Haystack skills')
1407
- .action(listSkills);
1408
- skills
1409
- .command('census-universe')
1410
- .description('Deterministically census Cloud Verifier source identities before mapping')
1411
- .option('--json', 'Machine-readable result')
1412
- .option('--root <path>', 'Repository root (default: current Git worktree)')
1413
- .action((opts) => runPublicCommand(async () => cloudVerifierIdentityCensusCommand(opts), opts.json));
1414
- skills
1415
- .command('data-store-drift')
1416
- .description('Check whether the detected data stores still match the checked-in map (per-PR drift gate)')
1417
- .option('--json', 'Machine-readable result')
1418
- .option('--root <path>', 'Repository root (default: current Git worktree)')
1419
- .option('--baseline <path>', 'Baseline map path (default: .haystack/cloud-verifier/data-stores.json)')
1420
- .option('--base-ref <ref>', 'Only run when the diff against this ref touches a store-relevant file')
1421
- .option('--update', 'Write the current detected stores as the new baseline')
1422
- .action((opts) => runPublicCommand(async () => cloudVerifierDataStoreDriftCommand(opts), opts.json));
1423
- skills
1424
- .command('validate-universe')
1425
- .description('Validate Cloud Verifier universe artifacts and stable-ID references')
1426
- .option('--json', 'Machine-readable result')
1427
- .action(async (opts) => {
1428
- try {
1429
- await cloudVerifierUniverseValidateCommand(opts);
1430
- }
1431
- catch (err) {
1432
- console.error(chalk.red('validate-universe failed:'), err instanceof Error ? err.message : err);
1433
- process.exit(1);
1434
- }
1435
- });
1436
- skills
1437
- .command('validate-behaviors')
1438
- .description('Validate customer actions and their exact runtime identities')
1439
- .option('--json', 'Machine-readable result')
1440
- .action(async (opts) => {
1441
- try {
1442
- await cloudVerifierBehaviorsValidateCommand(opts);
1443
- }
1444
- catch (err) {
1445
- console.error(chalk.red('validate-behaviors failed:'), err instanceof Error ? err.message : err);
1446
- process.exit(1);
1447
- }
1448
- });
1449
- skills
1450
- .command('scaffold-provisional-universe')
1451
- .description('Create a compact missing-authority receipt from strict JSON facts')
1452
- .requiredOption('--input <path>', 'Strict provisional-universe JSON input file')
1453
- .action((opts) => runPublicCommand(async () => scaffoldProvisionalUniverseCommand(opts), true));
1454
- skills
1455
- .command('prepare-universe-review')
1456
- .description('Create a tracked-source snapshot with conventional test paths removed')
1457
- .option('--json', 'Machine-readable isolation result and snapshot manifest')
1458
- .option('--exclude-inactive-submodule <path...>', 'Omit exact tracked gitlink paths explicitly known to be inactive')
1459
- .option('--omit-unscannable-files', 'Omit files whose test content cannot be ruled out, instead of blocking the review. '
1460
- + 'The reviewer still never sees a test; the omissions are recorded as uncovered scope')
1461
- .option('--full-receipt', 'Include complete non-blocking path receipts in JSON output')
1462
- .action((opts) => runPublicCommand(() => prepareUniverseReviewCommand(opts), opts.json));
1463
- const hooks = program
1464
- .command('hooks')
1465
- .description('Manage git hooks for AI agent quality checks');
1466
- hooks
1467
- .command('install')
1468
- .description('Install Haystack git hooks')
1469
- .option('-f, --force', 'Overwrite existing hooks')
1470
- .addHelpText('after', `
1471
- This installs:
1472
- • Git hooks for AI agent quality checks (pre-commit, commit-msg, etc.)
1473
- • Agent context detector (identifies AI agent sessions)
1474
- • Truncation checker (prevents code truncation by LLMs)
1475
-
1476
- Hooks are installed to <repo>/hooks/ and git is configured to use them.
1477
-
1478
- Examples:
1479
- haystack hooks install --force # Overwrite existing hooks
1480
- `)
1481
- .action(hooksInstall);
1482
- hooks
1483
- .command('status')
1484
- .description('Check hooks installation status')
1485
- .action(hooksStatus);
1486
- hooks
1487
- .command('install-session')
1488
- .description('Install session-start and Stop hooks for coding CLIs')
1489
- .option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
1490
- .option('--shared', 'Write Claude Code hooks to the committed .claude/settings.json instead of settings.local.json')
1491
- .addHelpText('after', `
1492
- Configures your coding CLI to run \`haystack triage --hook\` on session start.
1493
- This shows pending PR analysis results when you open a new terminal session.
1494
- Claude Code also runs \`haystack verify precompute --hook\` when a session stops,
1495
- which builds and freezes the changed app in the background so that
1496
- \`haystack verify\` starts from there. It never crawls; \`haystack verify\` does.
1497
-
1498
- Claude Code: Native SessionStart and Stop hooks (.claude/settings.local.json;
1499
- --shared writes the committed .claude/settings.json)
1500
- Codex CLI: AGENTS.md instructions
1501
- Gemini CLI: GEMINI.md instructions
1502
-
1503
- Examples:
1504
- haystack hooks install-session # Auto-detect CLIs
1505
- haystack hooks install-session --cli claude # Claude Code only
1506
- haystack hooks install-session --cli all # All detected CLIs
1507
- `)
1508
- .action(installSessionHooks);
1509
- hooks
1510
- .command('session-status')
1511
- .description('Check session hook installation status')
1512
- .action(sessionHooksStatus);
1513
- // Policy subcommands
1514
- const policy = program
1515
- .command('policy')
1516
- .description('Manage review policies (.haystack/review-policy.md)');
1517
- policy
1518
- .command('list')
1519
- .description('List all review policies')
1520
- .action(listPolicies);
1521
- policy
1522
- .command('add [name]')
1523
- .description('Add a new review policy interactively')
1524
- .addHelpText('after', `
1525
- This command prompts for:
1526
- • Policy name (e.g., "Infrastructure changes")
1527
- • File patterns (comma-separated globs, e.g., "terraform/**,*.tf")
1528
- • Severity (critical, high, medium, low)
1529
- • Reason (why this requires human review)
1530
-
1531
- Examples:
1532
- haystack policy add # Interactive add
1533
- haystack policy add "Database changes" # Start with name
1534
- `)
1535
- .action(addPolicy);
1536
- policy
1537
- .command('remove <name>')
1538
- .description('Remove a review policy by name')
1539
- .action(removePolicy);
1540
- policy
1541
- .command('add-instruction [text]')
1542
- .description('Add a semantic review instruction')
1543
- .addHelpText('after', `
1544
- Instructions are natural language directives that override default review behavior.
1545
- They apply to all PRs (not file-pattern-gated).
1546
-
1547
- Examples:
1548
- haystack policy add-instruction "Never flag weak test coverage as needing review"
1549
- haystack policy add-instruction # Interactive prompt
1550
- `)
1551
- .action(addInstruction);
1552
- policy
1553
- .command('init')
1554
- .description('Create initial review-policy.md with example policies')
1555
- .option('-f, --force', 'Overwrite existing file')
1556
- .addHelpText('after', `
1557
- Creates .haystack/review-policy.md with sensible defaults:
1558
- • Infrastructure changes (terraform, pulumi, cdk)
1559
- • Secret files (*.secret*, *.env*, credentials)
1560
- • CI/CD pipelines (GitHub Actions, GitLab CI, etc.)
1561
-
1562
- The generated policies appear in Haystack's "Human Review Needed"
1563
- section when a PR touches matching files.
1564
- `)
1565
- .action(initPolicies);
1566
- // ─── webhooks ────────────────────────────────────────────────────────────────
1567
- //
1568
- // `haystack webhooks ...` — register outbound webhook subscriptions and inspect
1569
- // delivery status. Talks to the haystack-webhooks worker.
1570
- //
1571
- // See docs/SPEC-CLI-FIRST-CLASS.md §1 for the wire protocol (HMAC, retries,
1572
- // idempotency).
1573
- const webhooks = program
1574
- .command('webhooks')
1575
- .description('Manage outbound webhooks');
1576
- webhooks
1577
- .command('register')
1578
- .description('Register a webhook URL for the current repo and mint a fresh HMAC secret')
1579
- .requiredOption('-u, --url <url>', 'Receiver URL (https only)')
1580
- .option('-e, --events <events>', 'Comma-separated event list', (val) => val.split(',').map((s) => s.trim()).filter(Boolean))
1581
- .option('--secret-env <name>', 'Environment-variable name where you\'ll stash the secret (recorded in .haystack.json)')
1582
- .option('--repo <owner/name>', 'Override the inferred repo')
1583
- .action(async (opts) => {
1584
- try {
1585
- await registerWebhook(opts);
1586
- }
1587
- catch (err) {
1588
- console.error(chalk.red('register failed:'), err instanceof Error ? err.message : err);
1589
- process.exit(1);
1590
- }
1591
- });
1592
- webhooks
1593
- .command('list')
1594
- .description('List active webhook registrations for the current repo')
1595
- .option('--repo <owner/name>', 'Override the inferred repo')
1596
- .option('--json', 'Machine-readable JSON output')
1597
- .action(async (opts) => {
1598
- try {
1599
- await listWebhooks(opts);
1600
- }
1601
- catch (err) {
1602
- console.error(chalk.red('list failed:'), err instanceof Error ? err.message : err);
1603
- process.exit(1);
1604
- }
1605
- });
1606
- webhooks
1607
- .command('rotate-secret <id>')
1608
- .description('Rotate the HMAC secret for a registration (prints the new secret once)')
1609
- .option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
1610
- .action(async (id, opts) => {
1611
- try {
1612
- await rotateWebhookSecret(id, opts.repo);
1613
- }
1614
- catch (err) {
1615
- console.error(chalk.red('rotate-secret failed:'), err instanceof Error ? err.message : err);
1616
- process.exit(1);
1617
- }
1618
- });
1619
- webhooks
1620
- .command('disable <id>')
1621
- .description('Disable a registration (stops delivery; existing in-flight retries continue)')
1622
- .option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
1623
- .action(async (id, opts) => {
1624
- try {
1625
- await setWebhookEnabled(id, false, opts.repo);
1626
- }
1627
- catch (err) {
1628
- console.error(chalk.red('disable failed:'), err instanceof Error ? err.message : err);
1629
- process.exit(1);
1630
- }
1631
- });
1632
- webhooks
1633
- .command('enable <id>')
1634
- .description('Re-enable a previously disabled registration')
1635
- .option('--repo <owner/name>', 'Repo the registration belongs to (for account resolution outside its checkout)')
1636
- .action(async (id, opts) => {
1637
- try {
1638
- await setWebhookEnabled(id, true, opts.repo);
1639
- }
1640
- catch (err) {
1641
- console.error(chalk.red('enable failed:'), err instanceof Error ? err.message : err);
1642
- process.exit(1);
1643
- }
1644
- });
1645
- webhooks
1646
- .command('deliveries')
1647
- .description('Show recent webhook delivery attempts for the current repo')
1648
- .option('--repo <owner/name>', 'Override the inferred repo')
1649
- .option('--limit <n>', 'Number of deliveries (default 20)', (v) => parseInt(v, 10))
1650
- .option('--json', 'Machine-readable JSON output')
1651
- .action(async (opts) => {
1652
- try {
1653
- await listDeliveries(opts);
1654
- }
1655
- catch (err) {
1656
- console.error(chalk.red('deliveries failed:'), err instanceof Error ? err.message : err);
1657
- process.exit(1);
1658
- }
1659
- });
1660
- webhooks
1661
- .command('replay <id>')
1662
- .description('Replay a previous delivery (idempotent via X-Haystack-Delivery)')
1663
- .option('--repo <owner/name>', 'Repo the delivery belongs to (for account resolution outside its checkout)')
1664
- .action(async (id, opts) => {
1665
- try {
1666
- await replayDelivery(id, opts.repo);
1667
- }
1668
- catch (err) {
1669
- console.error(chalk.red('replay failed:'), err instanceof Error ? err.message : err);
1670
- process.exit(1);
1671
- }
1672
- });
1673
- // ─── admin ───────────────────────────────────────────────────────────────────
1674
- // Operator commands. The only credential is the operator's Cloudflare Access
1675
- // token (`cloudflared access token`); exit 3 means an epoch conflict (409).
1676
862
  async function runAdminCommand(action) {
1677
863
  try {
1678
864
  process.exitCode = await action();
@@ -1682,7 +868,7 @@ async function runAdminCommand(action) {
1682
868
  process.exitCode = 1;
1683
869
  }
1684
870
  }
1685
- const admin = program.command('admin').description('Haystack operator commands (Cloudflare Access)');
871
+ const admin = program.command('admin', { hidden: true }).description('Haystack operator commands (Cloudflare Access)');
1686
872
  const fleetPolicy = admin
1687
873
  .command('fleet-policy')
1688
874
  .description('Fleet policy authority in D1: approved policies per tenant and repository')
@@ -1746,86 +932,6 @@ fleetPolicy
1746
932
  const { fleetPolicyHistoryCommand } = await import('./commands/fleet-policy.js');
1747
933
  await runAdminCommand(() => fleetPolicyHistoryCommand(repository, options));
1748
934
  });
1749
- // ─── rules ───────────────────────────────────────────────────────────────────
1750
- const rules = program.command('rules').description('Manage .haystack/pr-rules.yml');
1751
- rules
1752
- .command('edit')
1753
- .description('Open .haystack/pr-rules.yml in $EDITOR (creates a starter if missing)')
1754
- .action(async () => {
1755
- try {
1756
- await editRulesCommand();
1757
- }
1758
- catch (err) {
1759
- console.error(chalk.red('edit failed:'), err instanceof Error ? err.message : err);
1760
- process.exit(1);
1761
- }
1762
- });
1763
- rules
1764
- .command('validate')
1765
- .description('Parse pr-rules.yml with the same parser the server uses')
1766
- .action(async () => {
1767
- try {
1768
- await validateRulesCommand();
1769
- }
1770
- catch (err) {
1771
- console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
1772
- process.exit(1);
1773
- }
1774
- });
1775
- // ─── system-map ──────────────────────────────────────────────────────────────
1776
- const systemMap = program
1777
- .command('system-map')
1778
- .description('Manage .haystack/system.yml — runtime facts that help Haystack QA run your system');
1779
- systemMap
1780
- .command('validate')
1781
- .description('Check .haystack/system.yml against the rules Haystack QA will apply')
1782
- .option('--json', 'Machine-readable result')
1783
- .addHelpText('after', `
1784
- The system map holds VERIFIED facts about how your system works — install/
1785
- build commands, how services boot and signal readiness, seed/reset commands,
1786
- dev-only test logins, external services that must never be touched live, and
1787
- prebuilt CI artifacts. Haystack QA uses these facts to run more of what it
1788
- already wants to test; the file never selects tests.
1789
-
1790
- Author it with the /map-your-system skill (haystack skills install), then run
1791
- this before committing. Checks: valid YAML + version, no test-selection keys,
1792
- no secret values, non-empty boot/login/run commands, artifact declarations.
1793
-
1794
- Examples:
1795
- haystack system-map validate
1796
- haystack system-map validate --json
1797
- `)
1798
- .action(async (opts) => {
1799
- try {
1800
- await systemMapValidateCommand(opts);
1801
- }
1802
- catch (err) {
1803
- console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
1804
- process.exit(1);
1805
- }
1806
- });
1807
- // ─── mcp ─────────────────────────────────────────────────────────────────────
1808
- program
1809
- .command('mcp')
1810
- .description('Run a stdio MCP server exposing Haystack tools to Claude Code, Cursor, etc.')
1811
- .action(async () => {
1812
- try {
1813
- // Lazy import: only this command needs @modelcontextprotocol/sdk, so we keep it
1814
- // out of the startup graph (see the note where the eager import used to be).
1815
- const { runMcpServer } = await import('./commands/mcp.js');
1816
- await runMcpServer();
1817
- }
1818
- catch (err) {
1819
- const code = err?.code;
1820
- if (code === 'ERR_MODULE_NOT_FOUND') {
1821
- console.error(chalk.red('mcp server failed:'), 'the @modelcontextprotocol/sdk package is missing. Reinstall the CLI to pull it in (e.g. `npm i -g @haystackeditor/cli@latest`).');
1822
- }
1823
- else {
1824
- console.error(chalk.red('mcp server failed:'), err instanceof Error ? err.message : err);
1825
- }
1826
- process.exit(1);
1827
- }
1828
- });
1829
935
  // Show help if no command provided
1830
936
  if (process.argv.length === 2) {
1831
937
  program.help();