@haystackeditor/cli 0.10.3 → 0.12.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 (42) hide show
  1. package/README.md +15 -16
  2. package/dist/assets/hooks/agent-context/detect.ts +180 -0
  3. package/dist/assets/hooks/agent-context/format.ts +1 -0
  4. package/dist/assets/hooks/agent-context/index.ts +2 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +14 -5
  6. package/dist/assets/hooks/agent-context/parsers/codex.ts +416 -0
  7. package/dist/assets/hooks/agent-context/tsconfig.json +1 -0
  8. package/dist/assets/hooks/agent-context/types.ts +1 -1
  9. package/dist/assets/hooks/package.json +2 -1
  10. package/dist/assets/hooks/scripts/pre-commit.sh +80 -2
  11. package/dist/assets/hooks/scripts/pre-push.sh +1 -1
  12. package/dist/assets/skills/submit.md +20 -0
  13. package/dist/commands/config.d.ts +4 -0
  14. package/dist/commands/config.js +94 -69
  15. package/dist/commands/dismiss.js +2 -1
  16. package/dist/commands/install-session-hooks.d.ts +3 -2
  17. package/dist/commands/install-session-hooks.js +27 -11
  18. package/dist/commands/pr-status.d.ts +5 -0
  19. package/dist/commands/pr-status.js +2 -2
  20. package/dist/commands/request-review.d.ts +15 -0
  21. package/dist/commands/request-review.js +95 -0
  22. package/dist/commands/setup.d.ts +1 -0
  23. package/dist/commands/setup.js +285 -2
  24. package/dist/commands/submit.d.ts +1 -0
  25. package/dist/commands/submit.js +12 -19
  26. package/dist/commands/triage.d.ts +16 -4
  27. package/dist/commands/triage.js +278 -179
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.js +73 -50
  30. package/dist/triage/prompts.js +13 -5
  31. package/dist/triage/runner.js +1 -1
  32. package/dist/triage/traces.js +9 -3
  33. package/dist/triage/types.d.ts +1 -1
  34. package/dist/types.d.ts +146 -26
  35. package/dist/types.js +44 -5
  36. package/dist/utils/analysis-api.d.ts +16 -0
  37. package/dist/utils/analysis-api.js +23 -5
  38. package/dist/utils/telemetry.d.ts +17 -0
  39. package/dist/utils/telemetry.js +109 -0
  40. package/package.json +1 -1
  41. package/dist/commands/check-pending.d.ts +0 -19
  42. package/dist/commands/check-pending.js +0 -217
@@ -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)',
@@ -166,10 +166,17 @@ export async function handleAgenticTool(tool) {
166
166
  /**
167
167
  * Check if auto-merge is enabled for the current repo.
168
168
  * Returns false if .haystack.json is missing or on error.
169
+ *
170
+ * NOTE: This does NOT go through loadHaystackConfig() because that function
171
+ * calls process.exit(1) when .haystack.json is missing — which is uncatchable
172
+ * by try/catch. We read the file directly so missing config safely returns false.
169
173
  */
170
174
  export async function isAutoMergeEnabled() {
171
175
  try {
172
- return getPreference('auto_merge');
176
+ const root = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
177
+ const raw = readFileSync(resolve(root, '.haystack.json'), 'utf-8');
178
+ const config = JSON.parse(raw);
179
+ return config.preferences?.auto_merge ?? false;
173
180
  }
174
181
  catch {
175
182
  return false;
@@ -228,77 +235,103 @@ export async function handleAutoMerge(action) {
228
235
  // ============================================================================
229
236
  // Wait-for-reviewers (repo-level via .haystack.json)
230
237
  // ============================================================================
231
- function getBabysitterConfig() {
238
+ /** Reverse map: bot username → friendly source name */
239
+ const BOT_USERNAME_TO_SOURCE = {};
240
+ for (const source of AI_REVIEWER_SOURCES) {
241
+ BOT_USERNAME_TO_SOURCE[AI_REVIEWER_BOT_USERNAMES[source]] = source;
242
+ }
243
+ function getMergeQueueConfig() {
232
244
  const { config, path } = loadHaystackConfig();
233
- const babysitter = config.babysitter ?? {};
234
- const reviewers = babysitter.expected_ai_reviewers ?? [];
235
- return { config, path, reviewers };
245
+ const mergeConfig = config.merge_queue ?? {};
246
+ const bots = mergeConfig.wait_for_reviewer?.bots ?? [];
247
+ return { config, path, bots };
236
248
  }
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;
249
+ function saveMergeQueueBots(config, configPath, bots, timeoutMinutes) {
250
+ const mergeConfig = config.merge_queue ?? {};
251
+ if (bots.length > 0) {
252
+ mergeConfig.wait_for_reviewer = {
253
+ bots,
254
+ ...(timeoutMinutes != null ? { timeout_minutes: timeoutMinutes } : mergeConfig.wait_for_reviewer?.timeout_minutes != null ? { timeout_minutes: mergeConfig.wait_for_reviewer.timeout_minutes } : {}),
255
+ };
256
+ }
257
+ else {
258
+ delete mergeConfig.wait_for_reviewer;
259
+ }
260
+ // Always write to merge_queue (new key)
261
+ config.merge_queue = mergeConfig;
241
262
  saveHaystackConfig(config, configPath);
242
263
  }
243
- function printAvailableReviewers(currentReviewers) {
244
- const currentSet = new Set(currentReviewers);
264
+ function printAvailableReviewers(currentBots) {
265
+ const currentSet = new Set(currentBots);
245
266
  console.log(chalk.dim('Available AI reviewer sources:'));
246
267
  for (const source of AI_REVIEWER_SOURCES) {
247
268
  const name = AI_REVIEWER_DISPLAY_NAMES[source];
248
- const active = currentSet.has(source);
269
+ const botUsername = AI_REVIEWER_BOT_USERNAMES[source];
270
+ const active = currentSet.has(botUsername);
249
271
  const marker = active ? chalk.green(' ✓ ') : ' ';
250
- console.log(`${marker}${chalk.cyan(source)} — ${name}`);
272
+ console.log(`${marker}${chalk.cyan(source)} — ${name} ${chalk.dim(`(${botUsername})`)}`);
251
273
  }
252
274
  console.log();
253
275
  }
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);
276
+ /** Resolve a source name (e.g. "cursor") to its GitHub bot username. Also accepts raw bot usernames. */
277
+ function resolveToBot(input) {
278
+ const lower = input.toLowerCase();
279
+ // Check if it's a known friendly source name
280
+ if (AI_REVIEWER_SOURCES.includes(lower)) {
281
+ return AI_REVIEWER_BOT_USERNAMES[lower];
261
282
  }
262
- return sources;
283
+ // Already a bot username (e.g. "cursor-bugbot[bot]")
284
+ return input;
285
+ }
286
+ function displayName(botUsername) {
287
+ const source = BOT_USERNAME_TO_SOURCE[botUsername];
288
+ if (source) {
289
+ return `${AI_REVIEWER_DISPLAY_NAMES[source]} (${botUsername})`;
290
+ }
291
+ return botUsername;
263
292
  }
264
293
  export async function handleWaitForReviewers(action, reviewers) {
265
294
  const normalizedAction = action?.toLowerCase();
266
295
  // Default: show status
267
296
  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})`)}`);
297
+ const { bots } = getMergeQueueConfig();
298
+ console.log(chalk.bold('\nWait for AI reviewers\n'));
299
+ if (bots.length > 0) {
300
+ console.log('Haystack will wait for ALL of these bots to post before showing PRs in the inbox:');
301
+ for (const bot of bots) {
302
+ console.log(` ${chalk.green(displayName(bot))}`);
275
303
  }
304
+ console.log();
305
+ console.log(chalk.dim('Note: AI reviewers with check runs (CodeRabbit, Greptile, Cursor BugBot, etc.)'));
306
+ console.log(chalk.dim('are detected automatically. Only configure review-only bots here (e.g. Copilot).'));
276
307
  }
277
308
  else {
278
- console.log('No external AI reviewers configured.');
279
- console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
309
+ console.log('No review-only AI reviewers configured.');
310
+ console.log(chalk.dim('AI reviewers with check runs are still detected automatically.'));
280
311
  }
281
312
  console.log();
282
- printAvailableReviewers(current);
283
- console.log(chalk.dim('Add with: haystack config wait-for-reviewers add <source>'));
313
+ printAvailableReviewers(bots);
314
+ console.log(chalk.dim('Add with: haystack config wait-for-reviewers add cursor coderabbit'));
315
+ console.log(chalk.dim('Or raw bot: haystack config wait-for-reviewers add cursor-bugbot[bot]'));
284
316
  console.log();
285
317
  return;
286
318
  }
287
319
  if (normalizedAction === 'add') {
288
320
  if (!reviewers || reviewers.length === 0) {
289
- console.error(chalk.red('\nSpecify at least one reviewer source to add.'));
290
- const { reviewers: current } = getBabysitterConfig();
321
+ console.error(chalk.red('\nSpecify at least one reviewer to add.'));
322
+ const { bots } = getMergeQueueConfig();
291
323
  console.log();
292
- printAvailableReviewers(current);
324
+ printAvailableReviewers(bots);
293
325
  process.exit(1);
326
+ return; // unreachable — satisfies TS narrowing
294
327
  }
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));
328
+ const resolved = reviewers.map(resolveToBot);
329
+ const { config, path, bots: current } = getMergeQueueConfig();
330
+ const merged = Array.from(new Set([...current, ...resolved]));
331
+ saveMergeQueueBots(config, path, merged);
332
+ const added = resolved.filter(b => !current.includes(b));
300
333
  if (added.length > 0) {
301
- console.log(chalk.green(`\nAdded: ${added.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
334
+ console.log(chalk.green(`\nAdded: ${added.map(displayName).join(', ')}`));
302
335
  }
303
336
  else {
304
337
  console.log(chalk.dim('\nAll specified reviewers were already configured.'));
@@ -308,17 +341,18 @@ export async function handleWaitForReviewers(action, reviewers) {
308
341
  }
309
342
  if (normalizedAction === 'remove') {
310
343
  if (!reviewers || reviewers.length === 0) {
311
- console.error(chalk.red('\nSpecify at least one reviewer source to remove.'));
344
+ console.error(chalk.red('\nSpecify at least one reviewer to remove.'));
312
345
  process.exit(1);
346
+ return; // unreachable — satisfies TS narrowing
313
347
  }
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));
348
+ const resolved = reviewers.map(resolveToBot);
349
+ const { config, path, bots: current } = getMergeQueueConfig();
350
+ const removeSet = new Set(resolved);
351
+ const remaining = current.filter(b => !removeSet.has(b));
352
+ saveMergeQueueBots(config, path, remaining);
353
+ const removed = resolved.filter(b => current.includes(b));
320
354
  if (removed.length > 0) {
321
- console.log(chalk.yellow(`\nRemoved: ${removed.map(s => AI_REVIEWER_DISPLAY_NAMES[s] ?? s).join(', ')}`));
355
+ console.log(chalk.yellow(`\nRemoved: ${removed.map(displayName).join(', ')}`));
322
356
  }
323
357
  else {
324
358
  console.log(chalk.dim('\nNone of the specified reviewers were configured.'));
@@ -327,28 +361,19 @@ export async function handleWaitForReviewers(action, reviewers) {
327
361
  return;
328
362
  }
329
363
  if (normalizedAction === 'clear') {
330
- const { config, path } = getBabysitterConfig();
331
- saveBabysitterReviewers(config, path, []);
364
+ const { config, path } = getMergeQueueConfig();
365
+ saveMergeQueueBots(config, path, []);
332
366
  console.log(chalk.yellow('\nCleared all expected AI reviewers.'));
333
367
  console.log(chalk.dim('The merge queue will not wait for any external reviewers.'));
334
368
  console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
335
369
  return;
336
370
  }
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);
371
+ // Unknown action — maybe the user passed reviewer names directly (implicit 'add')
372
+ const allNames = [action, ...(reviewers ?? [])].filter(Boolean);
373
+ const resolved = allNames.map(resolveToBot);
374
+ const { config, path, bots: current } = getMergeQueueConfig();
375
+ const merged = Array.from(new Set([...current, ...resolved]));
376
+ saveMergeQueueBots(config, path, merged);
377
+ console.log(chalk.green(`\nAdded: ${resolved.map(displayName).join(', ')}`));
378
+ console.log(chalk.dim('Commit .haystack.json to share with your team.\n'));
354
379
  }
@@ -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
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * install-session-hooks — Wire up session-start hooks for coding CLIs.
3
3
  *
4
- * When a CLI session starts, `haystack check-pending --hook` runs automatically
5
- * and shows the user whether their pending PRs are "Good to merge" or "Need input".
4
+ * When a CLI session starts, `haystack triage --hook` runs automatically
5
+ * and shows the user whether their last submitted PR is "Good to merge" or
6
+ * "Needs input", with the Haystack rating and auto-fixer status.
6
7
  *
7
8
  * Supported CLIs:
8
9
  * - Claude Code: Native SessionStart hook in .claude/settings.json
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * install-session-hooks — Wire up session-start hooks for coding CLIs.
3
3
  *
4
- * When a CLI session starts, `haystack check-pending --hook` runs automatically
5
- * and shows the user whether their pending PRs are "Good to merge" or "Need input".
4
+ * When a CLI session starts, `haystack triage --hook` runs automatically
5
+ * and shows the user whether their last submitted PR is "Good to merge" or
6
+ * "Needs input", with the Haystack rating and auto-fixer status.
6
7
  *
7
8
  * Supported CLIs:
8
9
  * - Claude Code: Native SessionStart hook in .claude/settings.json
@@ -41,7 +42,7 @@ function detectInstalledCLIs() {
41
42
  // ============================================================================
42
43
  // Claude Code: Native SessionStart hook
43
44
  // ============================================================================
44
- const HAYSTACK_HOOK_COMMAND = 'haystack check-pending --hook';
45
+ const HAYSTACK_HOOK_COMMAND = 'haystack triage --hook';
45
46
  function installClaudeHook(gitRoot) {
46
47
  const settingsDir = join(gitRoot, '.claude');
47
48
  const settingsPath = join(settingsDir, 'settings.json');
@@ -61,13 +62,28 @@ function installClaudeHook(gitRoot) {
61
62
  // Ensure SessionStart array exists
62
63
  const sessionStart = (hooks.SessionStart || []);
63
64
  hooks.SessionStart = sessionStart;
64
- // Check if haystack hook already installed
65
- const alreadyInstalled = sessionStart.some(entry => entry.hooks?.some(h => h.command?.includes('haystack check-pending')));
66
- if (alreadyInstalled) {
67
- console.log(chalk.dim(' Claude Code hook already installed.'));
65
+ // Check if haystack hook already installed (current or legacy)
66
+ const isHaystackHook = (h) => h.command?.includes('haystack triage') || h.command?.includes('haystack check-pending');
67
+ const existingIndex = sessionStart.findIndex(entry => entry.hooks?.some(h => isHaystackHook(h)));
68
+ if (existingIndex !== -1) {
69
+ const entry = sessionStart[existingIndex];
70
+ const isLegacy = entry.hooks?.some(h => h.command?.includes('haystack check-pending'));
71
+ if (isLegacy) {
72
+ // Migrate legacy hook in-place
73
+ for (const h of entry.hooks || []) {
74
+ if (h.command?.includes('haystack check-pending')) {
75
+ h.command = HAYSTACK_HOOK_COMMAND;
76
+ }
77
+ }
78
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
79
+ console.log(chalk.green(' Claude Code hook migrated from check-pending to triage.'));
80
+ }
81
+ else {
82
+ console.log(chalk.dim(' Claude Code hook already installed.'));
83
+ }
68
84
  return true;
69
85
  }
70
- // Add the hook
86
+ // Add the hook (fresh install)
71
87
  sessionStart.push({
72
88
  matcher: '',
73
89
  hooks: [
@@ -92,7 +108,7 @@ function checkClaudeHook(gitRoot) {
92
108
  try {
93
109
  const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
94
110
  const sessionStart = settings?.hooks?.SessionStart || [];
95
- return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack check-pending')));
111
+ return sessionStart.some((entry) => entry.hooks?.some((h) => h.command?.includes('haystack triage') || h.command?.includes('haystack check-pending')));
96
112
  }
97
113
  catch {
98
114
  return false;
@@ -109,7 +125,7 @@ ${CODEX_HOOK_MARKER}
109
125
  When starting a new session, run this command first to check for pending PR analysis results:
110
126
 
111
127
  \`\`\`bash
112
- haystack check-pending --hook
128
+ haystack triage --hook
113
129
  \`\`\`
114
130
 
115
131
  This shows whether your submitted PRs are "Good to merge" or "Need your input".
@@ -157,7 +173,7 @@ ${GEMINI_HOOK_MARKER}
157
173
  When starting a new session, run this command first to check for pending PR analysis results:
158
174
 
159
175
  \`\`\`bash
160
- haystack check-pending --hook
176
+ haystack triage --hook
161
177
  \`\`\`
162
178
 
163
179
  This shows whether your submitted PRs are "Good to merge" or "Need your input".
@@ -13,4 +13,9 @@
13
13
  export interface PRStatusOptions {
14
14
  json?: boolean;
15
15
  }
16
+ export declare const BUCKET_DISPLAY: Record<string, {
17
+ label: string;
18
+ color: (s: string) => string;
19
+ icon: string;
20
+ }>;
16
21
  export declare function prStatusCommand(identifier: string, options: PRStatusOptions): Promise<void>;
@@ -46,9 +46,9 @@ function parsePRIdentifier(identifier) {
46
46
  // ============================================================================
47
47
  // Display helpers
48
48
  // ============================================================================
49
- const BUCKET_DISPLAY = {
49
+ export const BUCKET_DISPLAY = {
50
50
  'analyzing': { label: 'Analyzing', color: chalk.blue, icon: '...' },
51
- 'auto-fixing': { label: 'Auto-fixing', color: chalk.blue, icon: '>>>' },
51
+ 'auto-fixing': { label: 'Auto-fixing (alpha)', color: chalk.blue, icon: '>>>' },
52
52
  'good-to-merge': { label: 'Good to Merge', color: chalk.green, icon: '[+]' },
53
53
  'failed-when-merging': { label: 'Failed When Merging', color: chalk.red, icon: '[!]' },
54
54
  'issues': { label: 'Issues Found', color: chalk.yellow, icon: '[?]' },
@@ -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>;