@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
@@ -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
- }