@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.
- package/README.md +15 -16
- package/dist/assets/hooks/agent-context/detect.ts +180 -0
- package/dist/assets/hooks/agent-context/format.ts +1 -0
- package/dist/assets/hooks/agent-context/index.ts +2 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +14 -5
- package/dist/assets/hooks/agent-context/parsers/codex.ts +416 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +1 -0
- package/dist/assets/hooks/agent-context/types.ts +1 -1
- package/dist/assets/hooks/package.json +2 -1
- package/dist/assets/hooks/scripts/pre-commit.sh +80 -2
- package/dist/assets/hooks/scripts/pre-push.sh +1 -1
- package/dist/assets/skills/submit.md +20 -0
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +94 -69
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/install-session-hooks.d.ts +3 -2
- package/dist/commands/install-session-hooks.js +27 -11
- package/dist/commands/pr-status.d.ts +5 -0
- package/dist/commands/pr-status.js +2 -2
- package/dist/commands/request-review.d.ts +15 -0
- package/dist/commands/request-review.js +95 -0
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +285 -2
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +12 -19
- package/dist/commands/triage.d.ts +16 -4
- package/dist/commands/triage.js +278 -179
- package/dist/index.d.ts +2 -1
- package/dist/index.js +73 -50
- package/dist/triage/prompts.js +13 -5
- package/dist/triage/runner.js +1 -1
- package/dist/triage/traces.js +9 -3
- package/dist/triage/types.d.ts +1 -1
- package/dist/types.d.ts +146 -26
- package/dist/types.js +44 -5
- package/dist/utils/analysis-api.d.ts +16 -0
- package/dist/utils/analysis-api.js +23 -5
- package/dist/utils/telemetry.d.ts +17 -0
- package/dist/utils/telemetry.js +109 -0
- package/package.json +1 -1
- package/dist/commands/check-pending.d.ts +0 -19
- package/dist/commands/check-pending.js +0 -217
package/dist/commands/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
234
|
-
const
|
|
235
|
-
return { config, path,
|
|
245
|
+
const mergeConfig = config.merge_queue ?? {};
|
|
246
|
+
const bots = mergeConfig.wait_for_reviewer?.bots ?? [];
|
|
247
|
+
return { config, path, bots };
|
|
236
248
|
}
|
|
237
|
-
function
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
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(
|
|
244
|
-
const currentSet = new Set(
|
|
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
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
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 {
|
|
269
|
-
console.log(chalk.bold('\
|
|
270
|
-
if (
|
|
271
|
-
console.log('
|
|
272
|
-
for (const
|
|
273
|
-
|
|
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
|
|
279
|
-
console.log(chalk.dim('
|
|
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(
|
|
283
|
-
console.log(chalk.dim('Add with:
|
|
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
|
|
290
|
-
const {
|
|
321
|
+
console.error(chalk.red('\nSpecify at least one reviewer to add.'));
|
|
322
|
+
const { bots } = getMergeQueueConfig();
|
|
291
323
|
console.log();
|
|
292
|
-
printAvailableReviewers(
|
|
324
|
+
printAvailableReviewers(bots);
|
|
293
325
|
process.exit(1);
|
|
326
|
+
return; // unreachable — satisfies TS narrowing
|
|
294
327
|
}
|
|
295
|
-
const
|
|
296
|
-
const { config, path,
|
|
297
|
-
const merged = Array.from(new Set([...current, ...
|
|
298
|
-
|
|
299
|
-
const added =
|
|
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(
|
|
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
|
|
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
|
|
315
|
-
const { config, path,
|
|
316
|
-
const removeSet = new Set(
|
|
317
|
-
const remaining = current.filter(
|
|
318
|
-
|
|
319
|
-
const removed =
|
|
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(
|
|
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 } =
|
|
331
|
-
|
|
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
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
}
|
package/dist/commands/dismiss.js
CHANGED
|
@@ -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
|
|
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
|
|
5
|
-
* and shows the user whether their
|
|
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
|
|
5
|
-
* and shows the user whether their
|
|
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
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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>;
|