@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
@@ -1,549 +0,0 @@
1
- /**
2
- * Config commands - manage preferences
3
- *
4
- * Repo-level preferences (auto_merge) are stored in .haystack.json.
5
- * User-level preferences (agentic_tool) are stored on the Haystack Platform API.
6
- */
7
- import chalk from 'chalk';
8
- import { readFileSync, writeFileSync } from 'fs';
9
- import { resolve } from 'path';
10
- import { execSync } from 'child_process';
11
- import { resolveAuthContext } from '../utils/auth.js';
12
- import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES, AI_REVIEWER_BOT_USERNAMES } from '../types.js';
13
- const API_BASE = 'https://haystackeditor.com/api/preferences';
14
- const REPO_PREFERENCE_DEFAULTS = {
15
- auto_merge: false,
16
- auto_fix: false,
17
- };
18
- const AGENTIC_TOOL_LABELS = {
19
- 'opencode': 'OpenCode (Haystack billing)',
20
- 'claude-code': 'Claude Code (your Claude Max subscription)',
21
- 'codex': 'Codex CLI (your ChatGPT subscription)',
22
- };
23
- // ============================================================================
24
- // .haystack.json helpers (repo-level preferences)
25
- // ============================================================================
26
- function findRepoRoot() {
27
- try {
28
- return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
29
- }
30
- catch {
31
- console.error(chalk.red('\nNot inside a git repository.\n'));
32
- process.exit(1);
33
- }
34
- }
35
- function loadHaystackConfig() {
36
- const root = findRepoRoot();
37
- const configPath = resolve(root, '.haystack.json');
38
- try {
39
- const raw = readFileSync(configPath, 'utf-8');
40
- return { config: JSON.parse(raw), path: configPath };
41
- }
42
- catch (err) {
43
- if (err.code === 'ENOENT') {
44
- console.error(chalk.red(`\nNo .haystack.json found at ${configPath}`));
45
- console.log(chalk.dim('Create one with: haystack init\n'));
46
- process.exit(1);
47
- }
48
- throw err;
49
- }
50
- }
51
- function saveHaystackConfig(config, configPath) {
52
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
53
- }
54
- function getPreference(key) {
55
- const { config } = loadHaystackConfig();
56
- return config.preferences?.[key] ?? REPO_PREFERENCE_DEFAULTS[key];
57
- }
58
- function setPreference(key, value) {
59
- const { config, path } = loadHaystackConfig();
60
- if (!config.preferences) {
61
- config.preferences = {};
62
- }
63
- config.preferences[key] = value;
64
- saveHaystackConfig(config, path);
65
- }
66
- // ============================================================================
67
- // API helpers (user-level preferences)
68
- // ============================================================================
69
- async function requireAuth() {
70
- try {
71
- const authContext = await resolveAuthContext();
72
- return authContext.token;
73
- }
74
- catch (err) {
75
- console.error(chalk.red(`\n${err.message}\n`));
76
- process.exit(1);
77
- }
78
- }
79
- async function apiRequest(method, token, body) {
80
- return fetch(API_BASE, {
81
- method,
82
- headers: {
83
- 'Authorization': `Bearer ${token}`,
84
- 'Content-Type': 'application/json',
85
- 'User-Agent': 'Haystack-CLI',
86
- },
87
- body: body ? JSON.stringify(body) : undefined,
88
- });
89
- }
90
- // ============================================================================
91
- // Agentic tool (user-level via API)
92
- // ============================================================================
93
- export async function getAgenticToolStatus() {
94
- const token = await requireAuth();
95
- console.log(chalk.dim('\nFetching preferences...\n'));
96
- try {
97
- const response = await apiRequest('GET', token);
98
- if (!response.ok) {
99
- if (response.status === 401) {
100
- console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
101
- process.exit(1);
102
- }
103
- throw new Error(`Failed to get preferences: ${response.status}`);
104
- }
105
- const data = await response.json();
106
- const currentTool = data.agentic_tool || 'opencode';
107
- console.log(chalk.bold('Agentic tool:'), chalk.cyan(AGENTIC_TOOL_LABELS[currentTool]));
108
- console.log();
109
- console.log(chalk.dim('Available options:'));
110
- for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
111
- const marker = key === currentTool ? chalk.green(' ✓ ') : ' ';
112
- console.log(`${marker}${chalk.dim(key)} - ${label}`);
113
- }
114
- console.log();
115
- console.log(chalk.dim('Change with: haystack config agentic-tool <tool>'));
116
- console.log();
117
- }
118
- catch (error) {
119
- console.error(chalk.red(`\nError: ${error.message}\n`));
120
- process.exit(1);
121
- }
122
- }
123
- export async function setAgenticTool(tool) {
124
- const token = await requireAuth();
125
- console.log(chalk.dim(`\nSetting agentic tool to ${tool}...`));
126
- try {
127
- const response = await apiRequest('PUT', token, { agentic_tool: tool });
128
- if (!response.ok) {
129
- if (response.status === 401) {
130
- console.error(chalk.red('Session expired. Run `haystack login` again.\n'));
131
- process.exit(1);
132
- }
133
- const error = await response.json().catch(() => ({}));
134
- throw new Error(error.error || `Failed to update preferences: ${response.status}`);
135
- }
136
- console.log(chalk.green(`\nAgentic tool set to: ${AGENTIC_TOOL_LABELS[tool]}\n`));
137
- if (tool === 'claude-code') {
138
- console.log(chalk.dim('Note: Requires Claude Max subscription.'));
139
- console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
140
- }
141
- else if (tool === 'codex') {
142
- console.log(chalk.dim('Note: Requires ChatGPT Plus, Pro, or Team subscription.'));
143
- console.log(chalk.dim('Connect your account in the dashboard settings.\n'));
144
- }
145
- }
146
- catch (error) {
147
- console.error(chalk.red(`\nError: ${error.message}\n`));
148
- process.exit(1);
149
- }
150
- }
151
- export async function handleAgenticTool(tool) {
152
- const validTools = ['opencode', 'claude-code', 'codex'];
153
- if (!tool || tool === 'status') {
154
- return getAgenticToolStatus();
155
- }
156
- const normalizedTool = tool.toLowerCase();
157
- if (!validTools.includes(normalizedTool)) {
158
- console.error(chalk.red(`\nUnknown tool: ${tool}`));
159
- console.log('\nValid options:');
160
- for (const [key, label] of Object.entries(AGENTIC_TOOL_LABELS)) {
161
- console.log(` ${chalk.cyan(key)} - ${label}`);
162
- }
163
- console.log('\nUsage: haystack config agentic-tool [opencode|claude-code|codex|status]\n');
164
- process.exit(1);
165
- }
166
- return setAgenticTool(normalizedTool);
167
- }
168
- // ============================================================================
169
- // Auto-merge (repo-level via .haystack.json)
170
- // ============================================================================
171
- /**
172
- * Surface unexpected errors when reading repo-level preferences while keeping
173
- * the silent fallback for the expected "no .haystack.json" case. ENOENT means
174
- * the user simply hasn't opted in; everything else (parse errors, permission
175
- * denied, not-a-git-repo) is a config problem the user should see — silently
176
- * defaulting to disabled would otherwise hide the misconfiguration.
177
- */
178
- function logPreferenceLookupFailure(preference, err) {
179
- if (err?.code === 'ENOENT')
180
- return;
181
- const message = err instanceof Error ? err.message : String(err);
182
- console.error(chalk.dim(`[haystack] Could not resolve ${preference} preference (${message}); defaulting to disabled.`));
183
- }
184
- /**
185
- * Check if auto-merge is enabled for the current repo.
186
- * Returns false if .haystack.json is missing or on error.
187
- *
188
- * NOTE: This does NOT go through loadHaystackConfig() because that function
189
- * calls process.exit(1) when .haystack.json is missing — which is uncatchable
190
- * by try/catch. We read the file directly so missing config safely returns false.
191
- */
192
- export async function isAutoMergeEnabled() {
193
- try {
194
- const root = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
195
- const raw = readFileSync(resolve(root, '.haystack.json'), 'utf-8');
196
- const config = JSON.parse(raw);
197
- return config.preferences?.auto_merge ?? false;
198
- }
199
- catch (err) {
200
- logPreferenceLookupFailure('auto_merge', err);
201
- return false;
202
- }
203
- }
204
- /** Read the two queue-side toggles that depend on auto-merge. We re-read the
205
- * file rather than calling getPreference() because those settings live under
206
- * `merge_queue.*` instead of `preferences.*`. Returns null fields when the
207
- * config is missing or can't be parsed — callers treat null as "not set".
208
- * Note: loadHaystackConfig() process.exit(1)s on ENOENT, so this catch only
209
- * fires for unexpected failures (parse errors, permission denied). We use the
210
- * same logging helper as the other preference readers so failures surface in
211
- * stderr instead of silently rolling back to defaults. */
212
- function readDependentToggles() {
213
- try {
214
- const { config } = loadHaystackConfig();
215
- return {
216
- autoResolveConflicts: config.merge_queue?.auto_resolve_conflicts ?? null,
217
- autoFixCiFailures: config.merge_queue?.auto_fix_ci_failures ?? null,
218
- };
219
- }
220
- catch (err) {
221
- logPreferenceLookupFailure('merge_queue dependent toggles', err);
222
- return { autoResolveConflicts: null, autoFixCiFailures: null };
223
- }
224
- }
225
- function printDependencyNote(autoMergeEnabled) {
226
- const deps = readDependentToggles();
227
- // Only mention dependents that are explicitly configured — silent for repos
228
- // that never set them. Both rely on auto-merge because they're merge-queue
229
- // features that only fire on enqueued PRs.
230
- const items = [];
231
- if (deps.autoResolveConflicts != null)
232
- items.push(`auto-resolve-conflicts (${deps.autoResolveConflicts ? 'on' : 'off'})`);
233
- if (deps.autoFixCiFailures != null)
234
- items.push(`auto-fix-ci-failures (${deps.autoFixCiFailures ? 'on' : 'off'})`);
235
- if (items.length === 0)
236
- return;
237
- if (autoMergeEnabled) {
238
- console.log(chalk.dim(`Tied to auto-merge: ${items.join(', ')}.`));
239
- }
240
- else {
241
- console.log(chalk.yellow(`Note: ${items.join(', ')} won't run while auto-merge is off — these are merge-queue features that only fire on enqueued PRs.`));
242
- }
243
- }
244
- export async function getAutoMergeStatus() {
245
- const enabled = getPreference('auto_merge');
246
- console.log(chalk.bold('\nAuto-merge:'), enabled
247
- ? chalk.green('enabled')
248
- : chalk.yellow('disabled'));
249
- console.log();
250
- if (enabled) {
251
- console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
252
- console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
253
- }
254
- else {
255
- console.log(chalk.dim('Auto-merge is disabled. PRs require manual merge.'));
256
- console.log(chalk.dim('Enable with: haystack config auto-merge on'));
257
- }
258
- printDependencyNote(enabled);
259
- console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
260
- console.log();
261
- }
262
- export async function enableAutoMerge() {
263
- setPreference('auto_merge', true);
264
- console.log(chalk.green('\nAuto-merge enabled.\n'));
265
- console.log(chalk.dim('PRs submitted via `haystack submit` will be auto-merged'));
266
- console.log(chalk.dim('when analysis finds no issues (safe to merge).'));
267
- printDependencyNote(true);
268
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
269
- }
270
- export async function disableAutoMerge() {
271
- setPreference('auto_merge', false);
272
- console.log(chalk.yellow('\nAuto-merge disabled.\n'));
273
- console.log(chalk.dim('PRs will require manual merge after analysis.'));
274
- console.log(chalk.dim('Re-enable with: haystack config auto-merge on'));
275
- printDependencyNote(false);
276
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
277
- }
278
- export async function handleAutoMerge(action) {
279
- switch (action?.toLowerCase()) {
280
- case 'on':
281
- case 'enable':
282
- case 'true':
283
- return enableAutoMerge();
284
- case 'off':
285
- case 'disable':
286
- case 'false':
287
- return disableAutoMerge();
288
- case 'status':
289
- case undefined:
290
- return getAutoMergeStatus();
291
- default:
292
- console.error(chalk.red(`\nUnknown action: ${action}`));
293
- console.log('\nUsage: haystack config auto-merge [on|off|status]\n');
294
- process.exit(1);
295
- }
296
- }
297
- // ============================================================================
298
- // Auto-fix (repo-level via .haystack.json) — alpha
299
- // ============================================================================
300
- /**
301
- * Check if auto-fix is enabled for the current repo.
302
- * Returns false if .haystack.json is missing or on error.
303
- *
304
- * Mirrors isAutoMergeEnabled() — direct file read so missing config returns
305
- * false instead of process.exit(1).
306
- */
307
- export async function isAutoFixEnabled() {
308
- try {
309
- const root = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
310
- const raw = readFileSync(resolve(root, '.haystack.json'), 'utf-8');
311
- const config = JSON.parse(raw);
312
- return config.preferences?.auto_fix ?? false;
313
- }
314
- catch (err) {
315
- logPreferenceLookupFailure('auto_fix', err);
316
- return false;
317
- }
318
- }
319
- export async function getAutoFixStatus() {
320
- const enabled = getPreference('auto_fix');
321
- console.log(chalk.bold('\nAuto-fix (alpha):'), enabled
322
- ? chalk.green('enabled')
323
- : chalk.yellow('disabled'));
324
- console.log();
325
- if (enabled) {
326
- console.log(chalk.dim('PRs submitted via `haystack submit` will be labeled'));
327
- console.log(chalk.dim('haystack:auto-fix. Analysis will dispatch the alpha'));
328
- console.log(chalk.dim('sandbox fixer for straightforward mechanical issues.'));
329
- }
330
- else {
331
- console.log(chalk.dim('Auto-fix is disabled. Issues surface in the Feed for review.'));
332
- console.log(chalk.dim('Enable with: haystack config auto-fix on'));
333
- }
334
- console.log(chalk.yellow('⚠ Auto-fix is alpha. Expect rough edges.'));
335
- console.log(chalk.dim('(Stored in .haystack.json — applies to all repo contributors)'));
336
- console.log();
337
- }
338
- export async function enableAutoFix() {
339
- setPreference('auto_fix', true);
340
- console.log(chalk.green('\nAuto-fix (alpha) enabled.\n'));
341
- console.log(chalk.dim('PRs submitted via `haystack submit` will be labeled'));
342
- console.log(chalk.dim('haystack:auto-fix. The sandbox fixer will attempt'));
343
- console.log(chalk.dim('straightforward mechanical fixes before issues hit the Feed.'));
344
- console.log(chalk.yellow('⚠ Auto-fix is alpha. Expect rough edges.'));
345
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
346
- }
347
- export async function disableAutoFix() {
348
- setPreference('auto_fix', false);
349
- console.log(chalk.yellow('\nAuto-fix disabled.\n'));
350
- console.log(chalk.dim('Issues will surface in the Feed for human review.'));
351
- console.log(chalk.dim('Re-enable with: haystack config auto-fix on'));
352
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
353
- }
354
- export async function handleAutoFix(action) {
355
- switch (action?.toLowerCase()) {
356
- case 'on':
357
- case 'enable':
358
- case 'true':
359
- return enableAutoFix();
360
- case 'off':
361
- case 'disable':
362
- case 'false':
363
- return disableAutoFix();
364
- case 'status':
365
- case undefined:
366
- return getAutoFixStatus();
367
- default:
368
- console.error(chalk.red(`\nUnknown action: ${action}`));
369
- console.log('\nUsage: haystack config auto-fix [on|off|status]\n');
370
- process.exit(1);
371
- }
372
- }
373
- // ============================================================================
374
- // Wait-for-reviewers (repo-level via .haystack.json)
375
- // ============================================================================
376
- /** Reverse map: bot username → friendly source name */
377
- const BOT_USERNAME_TO_SOURCE = {};
378
- for (const source of AI_REVIEWER_SOURCES) {
379
- BOT_USERNAME_TO_SOURCE[AI_REVIEWER_BOT_USERNAMES[source]] = source;
380
- }
381
- function getMergeQueueConfig() {
382
- const { config, path } = loadHaystackConfig();
383
- const mergeConfig = config.merge_queue ?? {};
384
- const bots = mergeConfig.wait_for_reviewer?.bots ?? [];
385
- return { config, path, bots };
386
- }
387
- function saveMergeQueueBots(config, configPath, bots, timeoutMinutes) {
388
- const mergeConfig = config.merge_queue ?? {};
389
- if (bots.length > 0) {
390
- mergeConfig.wait_for_reviewer = {
391
- bots,
392
- ...(timeoutMinutes != null ? { timeout_minutes: timeoutMinutes } : mergeConfig.wait_for_reviewer?.timeout_minutes != null ? { timeout_minutes: mergeConfig.wait_for_reviewer.timeout_minutes } : {}),
393
- };
394
- }
395
- else {
396
- delete mergeConfig.wait_for_reviewer;
397
- }
398
- // Always write to merge_queue (new key)
399
- config.merge_queue = mergeConfig;
400
- saveHaystackConfig(config, configPath);
401
- }
402
- function printAvailableReviewers(currentBots) {
403
- const currentSet = new Set(currentBots);
404
- console.log(chalk.dim('Available AI reviewer sources:'));
405
- for (const source of AI_REVIEWER_SOURCES) {
406
- const name = AI_REVIEWER_DISPLAY_NAMES[source];
407
- const botUsername = AI_REVIEWER_BOT_USERNAMES[source];
408
- const active = currentSet.has(botUsername);
409
- const marker = active ? chalk.green(' ✓ ') : ' ';
410
- console.log(`${marker}${chalk.cyan(source)} — ${name} ${chalk.dim(`(${botUsername})`)}`);
411
- }
412
- console.log();
413
- }
414
- /**
415
- * Resolve a source name (e.g. "cursor") to its GitHub bot username. Raw bot
416
- * usernames (anything ending in `[bot]`) pass through unchanged.
417
- *
418
- * Unknown names are REJECTED, not stored: this value must exactly match a
419
- * GitHub bot login or the merge queue waits forever for a reviewer that can
420
- * never post. (A typo used to be saved verbatim and reported as success.)
421
- */
422
- export function resolveToBot(input) {
423
- const lower = input.toLowerCase();
424
- if (AI_REVIEWER_SOURCES.includes(lower)) {
425
- return AI_REVIEWER_BOT_USERNAMES[lower];
426
- }
427
- if (input.endsWith('[bot]')) {
428
- return input;
429
- }
430
- throw new Error(`Unknown reviewer: "${input}".\n\n` +
431
- `Use a friendly name (${AI_REVIEWER_SOURCES.join(', ')})\n` +
432
- `or an exact GitHub bot username ending in [bot] (e.g. cursor-bugbot[bot]).`);
433
- }
434
- /** Like resolveToBot, but unknown names pass through verbatim (for `remove`). */
435
- function resolveToBotLenient(input) {
436
- const lower = input.toLowerCase();
437
- if (AI_REVIEWER_SOURCES.includes(lower)) {
438
- return AI_REVIEWER_BOT_USERNAMES[lower];
439
- }
440
- return input;
441
- }
442
- function displayName(botUsername) {
443
- const source = BOT_USERNAME_TO_SOURCE[botUsername];
444
- if (source) {
445
- return `${AI_REVIEWER_DISPLAY_NAMES[source]} (${botUsername})`;
446
- }
447
- return botUsername;
448
- }
449
- export async function handleWaitForReviewers(action, reviewers) {
450
- const normalizedAction = action?.toLowerCase();
451
- // Default: show status
452
- if (!normalizedAction || normalizedAction === 'list' || normalizedAction === 'status') {
453
- const { bots } = getMergeQueueConfig();
454
- console.log(chalk.bold('\nWait for AI reviewers\n'));
455
- if (bots.length > 0) {
456
- console.log('Haystack will wait for ALL of these bots to post before showing PRs in the inbox:');
457
- for (const bot of bots) {
458
- console.log(` ${chalk.green(displayName(bot))}`);
459
- }
460
- console.log();
461
- console.log(chalk.dim('Note: AI reviewers with check runs (CodeRabbit, Greptile, Cursor BugBot, etc.)'));
462
- console.log(chalk.dim('are detected automatically. Only configure review-only bots here (e.g. Copilot).'));
463
- }
464
- else {
465
- console.log('No review-only AI reviewers configured.');
466
- console.log(chalk.dim('AI reviewers with check runs are still detected automatically.'));
467
- }
468
- console.log();
469
- printAvailableReviewers(bots);
470
- console.log(chalk.dim('Add with: haystack config wait-for-reviewers add cursor coderabbit'));
471
- console.log(chalk.dim('Or raw bot: haystack config wait-for-reviewers add cursor-bugbot[bot]'));
472
- console.log();
473
- return;
474
- }
475
- if (normalizedAction === 'add') {
476
- if (!reviewers || reviewers.length === 0) {
477
- console.error(chalk.red('\nSpecify at least one reviewer to add.'));
478
- const { bots } = getMergeQueueConfig();
479
- console.log();
480
- printAvailableReviewers(bots);
481
- process.exit(1);
482
- return; // unreachable — satisfies TS narrowing
483
- }
484
- let resolved;
485
- try {
486
- resolved = reviewers.map(resolveToBot);
487
- }
488
- catch (err) {
489
- console.error(chalk.red(`\n${err.message}\n`));
490
- process.exit(1);
491
- return; // unreachable — satisfies TS narrowing
492
- }
493
- const { config, path, bots: current } = getMergeQueueConfig();
494
- const merged = Array.from(new Set([...current, ...resolved]));
495
- saveMergeQueueBots(config, path, merged);
496
- const added = resolved.filter(b => !current.includes(b));
497
- if (added.length > 0) {
498
- console.log(chalk.green(`\nAdded: ${added.map(displayName).join(', ')}`));
499
- }
500
- else {
501
- console.log(chalk.dim('\nAll specified reviewers were already configured.'));
502
- }
503
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
504
- return;
505
- }
506
- if (normalizedAction === 'remove') {
507
- if (!reviewers || reviewers.length === 0) {
508
- console.error(chalk.red('\nSpecify at least one reviewer to remove.'));
509
- process.exit(1);
510
- return; // unreachable — satisfies TS narrowing
511
- }
512
- // Lenient on remove: stored entries may predate strict validation
513
- // (arbitrary strings used to be saved verbatim), and removing them must
514
- // stay possible.
515
- const resolved = reviewers.map(resolveToBotLenient);
516
- const { config, path, bots: current } = getMergeQueueConfig();
517
- const removeSet = new Set(resolved);
518
- const remaining = current.filter(b => !removeSet.has(b));
519
- saveMergeQueueBots(config, path, remaining);
520
- const removed = resolved.filter(b => current.includes(b));
521
- if (removed.length > 0) {
522
- console.log(chalk.yellow(`\nRemoved: ${removed.map(displayName).join(', ')}`));
523
- }
524
- else {
525
- console.log(chalk.dim('\nNone of the specified reviewers were configured.'));
526
- }
527
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
528
- return;
529
- }
530
- if (normalizedAction === 'clear') {
531
- const { config, path } = getMergeQueueConfig();
532
- saveMergeQueueBots(config, path, []);
533
- console.log(chalk.yellow('\nCleared all expected AI reviewers.'));
534
- console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
535
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
536
- return;
537
- }
538
- // Unknown action — reject it. This used to fall through to an implicit
539
- // "add", so a typo'd action (`wait-for-reviewers ad cursor`) silently
540
- // stored a bogus bot named "ad" and reported success.
541
- console.error(chalk.red(`\nUnknown action: "${action}".`));
542
- console.log('\nActions:');
543
- console.log(' list Show configured reviewer bots (default)');
544
- console.log(' add <names> Add one or more reviewer bots');
545
- console.log(' remove <names> Remove one or more reviewer bots');
546
- console.log(' clear Remove all reviewer bots');
547
- console.log(chalk.dim('\nExample: haystack config wait-for-reviewers add cursor\n'));
548
- process.exit(1);
549
- }