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