@haystackeditor/cli 0.25.1 → 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 (97) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/app-config.js +25 -1
  3. package/dist/commands/capture-brief.js +41 -32
  4. package/dist/commands/feedback.js +66 -0
  5. package/dist/commands/init-telemetry.js +53 -7
  6. package/dist/commands/init.js +7 -2
  7. package/dist/commands/lockfile-pin.js +307 -0
  8. package/dist/commands/verify.js +68 -13
  9. package/dist/index.js +27 -923
  10. package/dist/schema.js +4 -10
  11. package/dist/utils/haystack-api.js +0 -36
  12. package/package.json +1 -5
  13. package/schemas/feedback.v1.json +13 -0
  14. package/schemas/pre-verify.v2.json +240 -0
  15. package/schemas/verify-raw.v1.json +1132 -0
  16. package/schemas/verify.v2.json +655 -0
  17. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  18. package/dist/assets/hooks/agent-context/format.ts +0 -100
  19. package/dist/assets/hooks/agent-context/index.ts +0 -41
  20. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  21. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  22. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  23. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  24. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  25. package/dist/assets/hooks/agent-context/types.ts +0 -58
  26. package/dist/assets/hooks/llm-rules-template.md +0 -59
  27. package/dist/assets/hooks/package-lock.json +0 -598
  28. package/dist/assets/hooks/package.json +0 -12
  29. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  30. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  31. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  32. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  33. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  34. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  35. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  36. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  37. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  38. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  39. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  40. package/dist/assets/skills/map-your-system.md +0 -143
  41. package/dist/assets/skills/submit.md +0 -200
  42. package/dist/commands/ask.js +0 -20
  43. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  44. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  45. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  46. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  47. package/dist/commands/cloud-verifier-materialization.js +0 -704
  48. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  49. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  50. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  51. package/dist/commands/cloud-verifier-universe.js +0 -10178
  52. package/dist/commands/config.js +0 -549
  53. package/dist/commands/design-verify.js +0 -311
  54. package/dist/commands/dismiss.js +0 -159
  55. package/dist/commands/hooks.js +0 -226
  56. package/dist/commands/inbox.js +0 -137
  57. package/dist/commands/mcp.js +0 -201
  58. package/dist/commands/policy.js +0 -371
  59. package/dist/commands/pr-status.js +0 -207
  60. package/dist/commands/pr.js +0 -105
  61. package/dist/commands/prepare-universe-review.js +0 -1092
  62. package/dist/commands/production-source-deny-policy.js +0 -100
  63. package/dist/commands/request-review.js +0 -74
  64. package/dist/commands/review.js +0 -191
  65. package/dist/commands/rules.js +0 -98
  66. package/dist/commands/scaffold-provisional-universe.js +0 -806
  67. package/dist/commands/setup.js +0 -1170
  68. package/dist/commands/skills.js +0 -447
  69. package/dist/commands/submit.js +0 -745
  70. package/dist/commands/system-map.js +0 -228
  71. package/dist/commands/triage.js +0 -598
  72. package/dist/commands/webhooks.js +0 -241
  73. package/dist/states.js +0 -46
  74. package/dist/tools/detect.js +0 -832
  75. package/dist/triage/astra.js +0 -202
  76. package/dist/triage/prompts.js +0 -188
  77. package/dist/triage/runner.js +0 -200
  78. package/dist/triage/types.js +0 -7
  79. package/dist/utils/action-output.js +0 -26
  80. package/dist/utils/analysis-api.js +0 -416
  81. package/dist/utils/design-verifier-api.js +0 -294
  82. package/dist/utils/design-verifier-history.js +0 -79
  83. package/dist/utils/design-verifier-result.js +0 -424
  84. package/dist/utils/github-api.js +0 -324
  85. package/dist/utils/pending-state.js +0 -86
  86. package/dist/utils/pr-ref.js +0 -56
  87. package/dist/utils/prompter.js +0 -328
  88. package/schemas/action.v1.json +0 -22
  89. package/schemas/ask.v1.json +0 -40
  90. package/schemas/inbox.v1.json +0 -27
  91. package/schemas/pr-status.v1.json +0 -61
  92. package/schemas/pr.v1.json +0 -97
  93. package/schemas/pr.v3.json +0 -45
  94. package/schemas/setup.v1.json +0 -75
  95. package/schemas/submit.v1.json +0 -90
  96. package/schemas/triage.v1.json +0 -103
  97. 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();
@@ -220,48 +170,6 @@ Examples:
220
170
  haystack init --yes --json --notes haystack-notes.json
221
171
  `)
222
172
  .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
173
  program
266
174
  .command('status')
267
175
  .description('Check if .haystack.json exists and is valid')
@@ -277,7 +185,8 @@ const verify = program
277
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')
278
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])
279
187
  .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`)')
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`)')
281
190
  .addHelpText('after', `
282
191
  Run inside a git checkout; nothing needs to be committed or pushed. It captures
283
192
  the checkout (committed and uncommitted changes) exactly as the stop hook does,
@@ -333,7 +242,7 @@ Examples:
333
242
  haystack verify history owner/repo --limit 20
334
243
  haystack verify hosted status cv_<48 lowercase hex characters> --wait
335
244
  `)
336
- .action(options => runPublicCommand(() => verifyCommand(options), options.json));
245
+ .action(options => runPublicCommand(() => verifyCommand(options), options.json || options.raw));
337
246
  program
338
247
  .command('pre-verify')
339
248
  .description('Before a check: what your change touches, and what could break in it')
@@ -347,9 +256,10 @@ same way, builds and prepares the change when nothing has yet (the turn-end
347
256
  hook usually has), and prints its brief: the functions the change touches, the
348
257
  changed ones and those one step away first (every one is in --json), and
349
258
  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.
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.
353
263
 
354
264
  Then hand back what you were asked to do and what to try:
355
265
  haystack verify --intent "<what you were asked to do, in the user's words>" --idea "<something to try>"
@@ -455,7 +365,7 @@ verify
455
365
  process.exitCode = 0;
456
366
  });
457
367
  const hostedVerify = verify
458
- .command('hosted')
368
+ .command('hosted', { hidden: true })
459
369
  .description('Run the production Cloud Verifier against exact GitHub commits');
460
370
  hostedVerify
461
371
  .command('start')
@@ -581,7 +491,7 @@ verify
581
491
  return runPublicCommand(() => verifyExploreCommand(run, caseId, { ...options, repo: options.repo }), options.json);
582
492
  });
583
493
  const caseBatch = program
584
- .command('case-batch')
494
+ .command('case-batch', { hidden: true })
585
495
  .description('Submit and poll product-fuzzing case batches on the hosted coordinator (CASE-BATCH-V1)');
586
496
  caseBatch
587
497
  .command('submit')
@@ -712,6 +622,22 @@ just those paths with --only.
712
622
  const { caseBatchBundleCommand } = await import('./commands/case-batch.js');
713
623
  return runPublicCommand(() => caseBatchBundleCommand(runId, options), options.json);
714
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));
715
641
  program
716
642
  .command('login')
717
643
  .description('Add or refresh a saved GitHub account (use --headless for CI tokens)')
@@ -800,171 +726,6 @@ authProgram
800
726
  .command('use <login>')
801
727
  .description('Set the active Haystack GitHub account')
802
728
  .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
729
  const telemetry = program.command('telemetry').description('Add privacy-safe production telemetry without an application SDK')
969
730
  .addHelpText('after', `
970
731
  Next.js bundles its server code, so instead of \`instrument\` add the CLI as a
@@ -1098,583 +859,6 @@ start prints why no stand-in was used and names the declared ids; upload again w
1098
859
  that dependency; when both apply, the newer upload is used.
1099
860
  `)
1100
861
  .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
862
  async function runAdminCommand(action) {
1679
863
  try {
1680
864
  process.exitCode = await action();
@@ -1684,7 +868,7 @@ async function runAdminCommand(action) {
1684
868
  process.exitCode = 1;
1685
869
  }
1686
870
  }
1687
- 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)');
1688
872
  const fleetPolicy = admin
1689
873
  .command('fleet-policy')
1690
874
  .description('Fleet policy authority in D1: approved policies per tenant and repository')
@@ -1748,86 +932,6 @@ fleetPolicy
1748
932
  const { fleetPolicyHistoryCommand } = await import('./commands/fleet-policy.js');
1749
933
  await runAdminCommand(() => fleetPolicyHistoryCommand(repository, options));
1750
934
  });
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
935
  // Show help if no command provided
1832
936
  if (process.argv.length === 2) {
1833
937
  program.help();