@haystackeditor/cli 0.10.3 → 0.11.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @haystackeditor/cli
2
2
 
3
- Set up Haystack for your project. When PRs are opened, Haystack automatically reviews them for bugs, intent drift, and rule violations — then routes them to the right place.
3
+ Set up Haystack for your project. When PRs are opened, Haystack automatically reviews them for bugs, instruction drift, and rule violations — then routes them to the right place.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -65,7 +65,7 @@ haystack logout
65
65
 
66
66
  ### `haystack submit`
67
67
 
68
- Create a PR from current changes. Runs pre-PR triage (code review, rules validation, intent drift), pushes your branch, and opens the PR.
68
+ Create a PR from current changes. Runs pre-PR triage (code review, rules validation, instruction drift), pushes your branch, and opens the PR.
69
69
 
70
70
  ```bash
71
71
  haystack submit # Triage -> create PR -> wait for analysis
@@ -163,12 +163,14 @@ haystack config auto-merge # Show current status
163
163
  haystack config auto-merge on # Enable auto-merge
164
164
  haystack config auto-merge off # Disable auto-merge
165
165
 
166
- # AI reviewer wait list
166
+ # AI reviewer wait list (merge queue waits for ALL configured bots before merging)
167
167
  haystack config wait-for-reviewers # Show status
168
168
  haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
169
169
  haystack config wait-for-reviewers add cursor coderabbit # Add multiple
170
170
  haystack config wait-for-reviewers remove cursor # Stop waiting
171
171
  haystack config wait-for-reviewers clear # Wait for none
172
+ # Also accepts raw GitHub bot usernames:
173
+ haystack config wait-for-reviewers add cursor-bugbot[bot]
172
174
  ```
173
175
 
174
176
  ### `haystack skills`
@@ -244,7 +246,7 @@ The `setup` wizard writes `.haystack.json` to your repos with discovered rules,
244
246
  1. Run `haystack setup` to configure your repos (or `haystack init` for local-only config)
245
247
  2. Install the [Haystack GitHub App](https://haystackeditor.com/github-app)
246
248
  3. When PRs are opened, Haystack automatically:
247
- - Analyzes the code for bugs, intent drift, and rule violations
249
+ - Analyzes the code for bugs, instruction drift, and rule violations
248
250
  - Reports results on the PR
249
251
  - Routes the PR to the right inbox tab (Good to Merge, Issues Found, etc.)
250
252
  - If auto-merge is enabled, clean PRs merge automatically
@@ -9,7 +9,7 @@ import { readFileSync, writeFileSync } from 'fs';
9
9
  import { resolve } from 'path';
10
10
  import { execSync } from 'child_process';
11
11
  import { loadToken } from './login.js';
12
- import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES } from '../types.js';
12
+ import { AI_REVIEWER_SOURCES, AI_REVIEWER_DISPLAY_NAMES, AI_REVIEWER_BOT_USERNAMES } from '../types.js';
13
13
  const API_BASE = 'https://haystackeditor.com/api/preferences';
14
14
  const AGENTIC_TOOL_LABELS = {
15
15
  'opencode': 'OpenCode (Haystack billing)',
@@ -228,50 +228,71 @@ export async function handleAutoMerge(action) {
228
228
  // ============================================================================
229
229
  // Wait-for-reviewers (repo-level via .haystack.json)
230
230
  // ============================================================================
231
- function getBabysitterConfig() {
231
+ /** Reverse map: bot username → friendly source name */
232
+ const BOT_USERNAME_TO_SOURCE = {};
233
+ for (const source of AI_REVIEWER_SOURCES) {
234
+ BOT_USERNAME_TO_SOURCE[AI_REVIEWER_BOT_USERNAMES[source]] = source;
235
+ }
236
+ function getMergeQueueConfig() {
232
237
  const { config, path } = loadHaystackConfig();
233
- const babysitter = config.babysitter ?? {};
234
- const reviewers = babysitter.expected_ai_reviewers ?? [];
235
- return { config, path, reviewers };
238
+ const mergeConfig = config.merge_queue ?? {};
239
+ const bots = mergeConfig.wait_for_reviewer?.bots ?? [];
240
+ return { config, path, bots };
236
241
  }
237
- function saveBabysitterReviewers(config, configPath, reviewers) {
238
- const babysitter = config.babysitter ?? {};
239
- babysitter.expected_ai_reviewers = reviewers.length > 0 ? reviewers : undefined;
240
- config.babysitter = babysitter;
242
+ function saveMergeQueueBots(config, configPath, bots, timeoutMinutes) {
243
+ const mergeConfig = config.merge_queue ?? {};
244
+ if (bots.length > 0) {
245
+ mergeConfig.wait_for_reviewer = {
246
+ bots,
247
+ ...(timeoutMinutes != null ? { timeout_minutes: timeoutMinutes } : mergeConfig.wait_for_reviewer?.timeout_minutes != null ? { timeout_minutes: mergeConfig.wait_for_reviewer.timeout_minutes } : {}),
248
+ };
249
+ }
250
+ else {
251
+ delete mergeConfig.wait_for_reviewer;
252
+ }
253
+ // Always write to merge_queue (new key)
254
+ config.merge_queue = mergeConfig;
241
255
  saveHaystackConfig(config, configPath);
242
256
  }
243
- function printAvailableReviewers(currentReviewers) {
244
- const currentSet = new Set(currentReviewers);
257
+ function printAvailableReviewers(currentBots) {
258
+ const currentSet = new Set(currentBots);
245
259
  console.log(chalk.dim('Available AI reviewer sources:'));
246
260
  for (const source of AI_REVIEWER_SOURCES) {
247
261
  const name = AI_REVIEWER_DISPLAY_NAMES[source];
248
- const active = currentSet.has(source);
262
+ const botUsername = AI_REVIEWER_BOT_USERNAMES[source];
263
+ const active = currentSet.has(botUsername);
249
264
  const marker = active ? chalk.green(' ✓ ') : ' ';
250
- console.log(`${marker}${chalk.cyan(source)} — ${name}`);
265
+ console.log(`${marker}${chalk.cyan(source)} — ${name} ${chalk.dim(`(${botUsername})`)}`);
251
266
  }
252
267
  console.log();
253
268
  }
254
- function validateReviewerSources(sources) {
255
- const validSet = new Set(AI_REVIEWER_SOURCES);
256
- const invalid = sources.filter(s => !validSet.has(s));
257
- if (invalid.length > 0) {
258
- console.error(chalk.red(`\nUnknown reviewer source(s): ${invalid.join(', ')}`));
259
- console.log(chalk.dim(`\nValid sources: ${AI_REVIEWER_SOURCES.join(', ')}`));
260
- process.exit(1);
269
+ /** Resolve a source name (e.g. "cursor") to its GitHub bot username. Also accepts raw bot usernames. */
270
+ function resolveToBot(input) {
271
+ const lower = input.toLowerCase();
272
+ // Check if it's a known friendly source name
273
+ if (AI_REVIEWER_SOURCES.includes(lower)) {
274
+ return AI_REVIEWER_BOT_USERNAMES[lower];
261
275
  }
262
- return sources;
276
+ // Already a bot username (e.g. "cursor-bugbot[bot]")
277
+ return input;
278
+ }
279
+ function displayName(botUsername) {
280
+ const source = BOT_USERNAME_TO_SOURCE[botUsername];
281
+ if (source) {
282
+ return `${AI_REVIEWER_DISPLAY_NAMES[source]} (${botUsername})`;
283
+ }
284
+ return botUsername;
263
285
  }
264
286
  export async function handleWaitForReviewers(action, reviewers) {
265
287
  const normalizedAction = action?.toLowerCase();
266
288
  // Default: show status
267
289
  if (!normalizedAction || normalizedAction === 'list' || normalizedAction === 'status') {
268
- const { reviewers: current } = getBabysitterConfig();
269
- console.log(chalk.bold('\nMerge queue — expected AI reviewers\n'));
270
- if (current.length > 0) {
271
- console.log('The merge queue will wait for these external reviewers before merging:');
272
- for (const source of current) {
273
- const name = AI_REVIEWER_DISPLAY_NAMES[source] ?? source;
274
- console.log(` ${chalk.green(name)} ${chalk.dim(`(${source})`)}`);
290
+ const { bots } = getMergeQueueConfig();
291
+ console.log(chalk.bold('\nMerge queue — wait for reviewers\n'));
292
+ if (bots.length > 0) {
293
+ console.log('The merge queue will wait for ALL of these bots to post before merging:');
294
+ for (const bot of bots) {
295
+ console.log(` ${chalk.green(displayName(bot))}`);
275
296
  }
276
297
  }
277
298
  else {
@@ -279,26 +300,28 @@ export async function handleWaitForReviewers(action, reviewers) {
279
300
  console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
280
301
  }
281
302
  console.log();
282
- printAvailableReviewers(current);
283
- console.log(chalk.dim('Add with: haystack config wait-for-reviewers add <source>'));
303
+ printAvailableReviewers(bots);
304
+ console.log(chalk.dim('Add with: haystack config wait-for-reviewers add cursor coderabbit'));
305
+ console.log(chalk.dim('Or raw bot: haystack config wait-for-reviewers add cursor-bugbot[bot]'));
284
306
  console.log();
285
307
  return;
286
308
  }
287
309
  if (normalizedAction === 'add') {
288
310
  if (!reviewers || reviewers.length === 0) {
289
- console.error(chalk.red('\nSpecify at least one reviewer source to add.'));
290
- const { reviewers: current } = getBabysitterConfig();
311
+ console.error(chalk.red('\nSpecify at least one reviewer to add.'));
312
+ const { bots } = getMergeQueueConfig();
291
313
  console.log();
292
- printAvailableReviewers(current);
314
+ printAvailableReviewers(bots);
293
315
  process.exit(1);
316
+ return; // unreachable — satisfies TS narrowing
294
317
  }
295
- const validated = validateReviewerSources(reviewers);
296
- const { config, path, reviewers: current } = getBabysitterConfig();
297
- const merged = Array.from(new Set([...current, ...validated]));
298
- saveBabysitterReviewers(config, path, merged);
299
- const added = validated.filter(s => !current.includes(s));
318
+ const resolved = reviewers.map(resolveToBot);
319
+ const { config, path, bots: current } = getMergeQueueConfig();
320
+ const merged = Array.from(new Set([...current, ...resolved]));
321
+ saveMergeQueueBots(config, path, merged);
322
+ const added = resolved.filter(b => !current.includes(b));
300
323
  if (added.length > 0) {
301
- console.log(chalk.green(`\nAdded: ${added.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
324
+ console.log(chalk.green(`\nAdded: ${added.map(displayName).join(', ')}`));
302
325
  }
303
326
  else {
304
327
  console.log(chalk.dim('\nAll specified reviewers were already configured.'));
@@ -308,17 +331,18 @@ export async function handleWaitForReviewers(action, reviewers) {
308
331
  }
309
332
  if (normalizedAction === 'remove') {
310
333
  if (!reviewers || reviewers.length === 0) {
311
- console.error(chalk.red('\nSpecify at least one reviewer source to remove.'));
334
+ console.error(chalk.red('\nSpecify at least one reviewer to remove.'));
312
335
  process.exit(1);
336
+ return; // unreachable — satisfies TS narrowing
313
337
  }
314
- const validated = validateReviewerSources(reviewers);
315
- const { config, path, reviewers: current } = getBabysitterConfig();
316
- const removeSet = new Set(validated);
317
- const remaining = current.filter(s => !removeSet.has(s));
318
- saveBabysitterReviewers(config, path, remaining);
319
- const removed = validated.filter(s => current.includes(s));
338
+ const resolved = reviewers.map(resolveToBot);
339
+ const { config, path, bots: current } = getMergeQueueConfig();
340
+ const removeSet = new Set(resolved);
341
+ const remaining = current.filter(b => !removeSet.has(b));
342
+ saveMergeQueueBots(config, path, remaining);
343
+ const removed = resolved.filter(b => current.includes(b));
320
344
  if (removed.length > 0) {
321
- console.log(chalk.yellow(`\nRemoved: ${removed.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
345
+ console.log(chalk.yellow(`\nRemoved: ${removed.map(displayName).join(', ')}`));
322
346
  }
323
347
  else {
324
348
  console.log(chalk.dim('\nNone of the specified reviewers were configured.'));
@@ -327,28 +351,19 @@ export async function handleWaitForReviewers(action, reviewers) {
327
351
  return;
328
352
  }
329
353
  if (normalizedAction === 'clear') {
330
- const { config, path } = getBabysitterConfig();
331
- saveBabysitterReviewers(config, path, []);
354
+ const { config, path } = getMergeQueueConfig();
355
+ saveMergeQueueBots(config, path, []);
332
356
  console.log(chalk.yellow('\nCleared all expected AI reviewers.'));
333
357
  console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
334
358
  console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
335
359
  return;
336
360
  }
337
- // Unknown action — maybe the user passed a reviewer name directly
338
- // Try treating action + reviewers as all reviewer names for 'add'
339
- const allNames = [action, ...(reviewers ?? [])];
340
- const validSet = new Set(AI_REVIEWER_SOURCES);
341
- if (allNames.every(n => validSet.has(n))) {
342
- // Treat as implicit 'add'
343
- const validated = validateReviewerSources(allNames);
344
- const { config, path, reviewers: current } = getBabysitterConfig();
345
- const merged = Array.from(new Set([...current, ...validated]));
346
- saveBabysitterReviewers(config, path, merged);
347
- console.log(chalk.green(`\nAdded: ${validated.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
348
- console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
349
- return;
350
- }
351
- console.error(chalk.red(`\nUnknown action: ${action}`));
352
- console.log('\nUsage: haystack config wait-for-reviewers [list|add|remove|clear] [sources...]\n');
353
- process.exit(1);
361
+ // Unknown action — maybe the user passed reviewer names directly (implicit 'add')
362
+ const allNames = [action, ...(reviewers ?? [])].filter(Boolean);
363
+ const resolved = allNames.map(resolveToBot);
364
+ const { config, path, bots: current } = getMergeQueueConfig();
365
+ const merged = Array.from(new Set([...current, ...resolved]));
366
+ saveMergeQueueBots(config, path, merged);
367
+ console.log(chalk.green(`\nAdded: ${resolved.map(displayName).join(', ')}`));
368
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
354
369
  }
@@ -161,7 +161,8 @@ async function runOverride(identifier, type, commandName) {
161
161
  console.log(chalk.green(`\n Review marked as not needed for ${prLabel}`));
162
162
  }
163
163
  console.log(chalk.dim(` Commit: ${headSha.slice(0, 7)}`));
164
- console.log(chalk.dim(' The PR will move to "Good to Merge" in the feed.\n'));
164
+ console.log(chalk.dim(' The PR will move to "Good to Merge" in the feed.'));
165
+ console.log(chalk.dim(' If a new commit introduces net-new issues, the PR will resurface.\n'));
165
166
  }
166
167
  // ============================================================================
167
168
  // Exported commands
@@ -0,0 +1,15 @@
1
+ /**
2
+ * request-review command — Tag a PR as needing human review after creation.
3
+ *
4
+ * haystack request-review <pr> [reviewer]
5
+ *
6
+ * Adds the `haystack:needs-review` label to the PR. Optionally requests
7
+ * review from a specific GitHub user. The PR will move to the
8
+ * "Needs Assignment" or "Awaiting Review" bucket in the Haystack feed.
9
+ *
10
+ * Accepts a PR identifier in several formats:
11
+ * 123 # Uses current repo's owner/repo
12
+ * owner/repo#123 # Fully qualified
13
+ * https://github.com/owner/repo/pull/123 # GitHub URL
14
+ */
15
+ export declare function requestReviewCommand(identifier: string, reviewer?: string): Promise<void>;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * request-review command — Tag a PR as needing human review after creation.
3
+ *
4
+ * haystack request-review <pr> [reviewer]
5
+ *
6
+ * Adds the `haystack:needs-review` label to the PR. Optionally requests
7
+ * review from a specific GitHub user. The PR will move to the
8
+ * "Needs Assignment" or "Awaiting Review" bucket in the Haystack feed.
9
+ *
10
+ * Accepts a PR identifier in several formats:
11
+ * 123 # Uses current repo's owner/repo
12
+ * owner/repo#123 # Fully qualified
13
+ * https://github.com/owner/repo/pull/123 # GitHub URL
14
+ */
15
+ import chalk from 'chalk';
16
+ import { loadToken } from './login.js';
17
+ import { parseRemoteUrl } from '../utils/git.js';
18
+ import { ensureHaystackLabels, addLabelsToIssue, requestReviewers, } from '../utils/github-api.js';
19
+ // ============================================================================
20
+ // PR identifier parsing (shared pattern with dismiss.ts)
21
+ // ============================================================================
22
+ function parsePRIdentifier(identifier) {
23
+ const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
24
+ if (urlMatch) {
25
+ return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
26
+ }
27
+ const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
28
+ if (qualifiedMatch) {
29
+ return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
30
+ }
31
+ const numberMatch = identifier.match(/^#?(\d+)$/);
32
+ if (numberMatch) {
33
+ const prNumber = parseInt(numberMatch[1], 10);
34
+ try {
35
+ const remote = parseRemoteUrl('origin');
36
+ return { owner: remote.owner, repo: remote.repo, prNumber };
37
+ }
38
+ catch {
39
+ throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
40
+ `Use the full format: haystack request-review owner/repo#${prNumber}`);
41
+ }
42
+ }
43
+ throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
44
+ `Accepted formats:\n` +
45
+ ` haystack request-review 123\n` +
46
+ ` haystack request-review owner/repo#123\n` +
47
+ ` haystack request-review https://github.com/owner/repo/pull/123`);
48
+ }
49
+ // ============================================================================
50
+ // Main command
51
+ // ============================================================================
52
+ export async function requestReviewCommand(identifier, reviewer) {
53
+ // Parse PR identifier
54
+ let pr;
55
+ try {
56
+ pr = parsePRIdentifier(identifier);
57
+ }
58
+ catch (err) {
59
+ console.error(chalk.red(`\n${err.message}\n`));
60
+ process.exit(1);
61
+ }
62
+ // Require auth
63
+ const token = await loadToken();
64
+ if (!token) {
65
+ console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
66
+ process.exit(1);
67
+ }
68
+ const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
69
+ console.log(chalk.dim(`\nRequesting review for ${prLabel}...`));
70
+ // Ensure the label exists in the repo, then add it to the PR
71
+ try {
72
+ await ensureHaystackLabels(pr.owner, pr.repo, ['haystack:needs-review']);
73
+ await addLabelsToIssue(pr.owner, pr.repo, pr.prNumber, ['haystack:needs-review']);
74
+ }
75
+ catch (err) {
76
+ console.error(chalk.red(`\nFailed to add label: ${err.message}\n`));
77
+ process.exit(1);
78
+ }
79
+ // Request specific reviewer if provided
80
+ if (reviewer) {
81
+ try {
82
+ await requestReviewers(pr.owner, pr.repo, pr.prNumber, [reviewer]);
83
+ console.log(chalk.green(`\n Review requested from ${reviewer} on ${prLabel}`));
84
+ }
85
+ catch (err) {
86
+ // Label was added successfully, just the reviewer request failed
87
+ console.log(chalk.green(`\n Marked ${prLabel} for review`));
88
+ console.log(chalk.yellow(` Could not request reviewer "${reviewer}": ${err.message}`));
89
+ }
90
+ }
91
+ else {
92
+ console.log(chalk.green(`\n Marked ${prLabel} for human review`));
93
+ }
94
+ console.log(chalk.dim(' The PR will appear in the "Needs Assignment" queue in the feed.\n'));
95
+ }
@@ -9,5 +9,6 @@
9
9
  * 5. Scan for review policies
10
10
  * 6. Review discovered items (toggle on/off)
11
11
  * 7. Confirm & write .haystack.json to selected repos
12
+ * 8. Install session tracking (Entire CLI + .entire/settings.json + local hooks)
12
13
  */
13
14
  export declare function setupCommand(): Promise<void>;