@haystackeditor/cli 0.8.1 → 0.10.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 +93 -87
- package/dist/assets/hooks/llm-rules-template.md +21 -0
- package/dist/assets/hooks/package.json +2 -2
- package/dist/assets/hooks/scripts/pre-push.sh +20 -0
- package/dist/assets/skills/prepare-haystack.md +323 -0
- package/dist/assets/skills/secrets.md +164 -0
- package/dist/assets/skills/setup-external-sandbox.md +243 -0
- package/dist/assets/skills/setup-haystack.md +639 -0
- package/dist/assets/skills/submit.md +154 -0
- package/dist/assets/templates/CLAUDE.md.snippet +42 -0
- package/dist/assets/templates/haystack.yml +193 -0
- package/dist/commands/check-pending.d.ts +19 -0
- package/dist/commands/check-pending.js +217 -0
- package/dist/commands/config.d.ts +13 -21
- package/dist/commands/config.js +278 -92
- package/dist/commands/dismiss.d.ts +17 -0
- package/dist/commands/dismiss.js +201 -0
- package/dist/commands/init.js +25 -28
- package/dist/commands/install-session-hooks.d.ts +16 -0
- package/dist/commands/install-session-hooks.js +302 -0
- package/dist/commands/login.js +1 -1
- package/dist/commands/policy.d.ts +31 -0
- package/dist/commands/policy.js +365 -0
- package/dist/commands/pr-status.d.ts +16 -0
- package/dist/commands/pr-status.js +188 -0
- package/dist/commands/setup.d.ts +13 -0
- package/dist/commands/setup.js +496 -0
- package/dist/commands/skills.d.ts +2 -2
- package/dist/commands/skills.js +51 -186
- package/dist/commands/submit.d.ts +23 -0
- package/dist/commands/submit.js +456 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +344 -4
- package/dist/tools/detect.d.ts +50 -0
- package/dist/tools/detect.js +853 -0
- package/dist/tools/fixtures.d.ts +38 -0
- package/dist/tools/fixtures.js +199 -0
- package/dist/tools/setup.d.ts +43 -0
- package/dist/tools/setup.js +597 -0
- package/dist/triage/prompts.d.ts +31 -0
- package/dist/triage/prompts.js +296 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +339 -0
- package/dist/triage/traces.d.ts +20 -0
- package/dist/triage/traces.js +305 -0
- package/dist/triage/types.d.ts +47 -0
- package/dist/triage/types.js +7 -0
- package/dist/types.d.ts +1387 -191
- package/dist/types.js +254 -2
- package/dist/utils/analysis-api.d.ts +108 -0
- package/dist/utils/analysis-api.js +194 -0
- package/dist/utils/config.js +1 -1
- package/dist/utils/git.d.ts +80 -0
- package/dist/utils/git.js +302 -0
- package/dist/utils/github-api.d.ts +83 -0
- package/dist/utils/github-api.js +266 -0
- package/dist/utils/pending-state.d.ts +40 -0
- package/dist/utils/pending-state.js +86 -0
- package/dist/utils/secrets.js +3 -3
- package/dist/utils/skill.js +257 -0
- package/package.json +11 -9
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* haystack policy - Manage review policies
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* list - List all review policies
|
|
6
|
+
* add - Add a new policy interactively
|
|
7
|
+
* remove - Remove a policy by name
|
|
8
|
+
* init - Create initial review-policy.md
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import inquirer from 'inquirer';
|
|
14
|
+
const POLICY_PATH = '.haystack/review-policy.md';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Markdown Parsing
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Parse review-policy.md content into ReviewPolicy objects and instructions
|
|
20
|
+
*/
|
|
21
|
+
function parsePolicyMarkdown(content) {
|
|
22
|
+
const policies = [];
|
|
23
|
+
const instructions = [];
|
|
24
|
+
// Split by H2 headings (## )
|
|
25
|
+
const sections = content.split(/^## /m).slice(1); // Skip content before first H2
|
|
26
|
+
for (const section of sections) {
|
|
27
|
+
const lines = section.trim().split('\n');
|
|
28
|
+
if (lines.length === 0)
|
|
29
|
+
continue;
|
|
30
|
+
// First line is the section name
|
|
31
|
+
const name = lines[0].trim();
|
|
32
|
+
if (!name)
|
|
33
|
+
continue;
|
|
34
|
+
// ## Instructions — collect bullet points as raw semantic directives
|
|
35
|
+
if (name.toLowerCase() === 'instructions') {
|
|
36
|
+
for (const line of lines.slice(1)) {
|
|
37
|
+
const trimmed = line.trim();
|
|
38
|
+
const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
|
|
39
|
+
if (bulletMatch) {
|
|
40
|
+
instructions.push(bulletMatch[1].trim());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
let paths = [];
|
|
46
|
+
let severity = 'medium';
|
|
47
|
+
let reason = '';
|
|
48
|
+
// Parse bullet points
|
|
49
|
+
for (const line of lines.slice(1)) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
// Match **Paths**: `pattern1`, `pattern2`
|
|
52
|
+
const pathsMatch = trimmed.match(/^\*\*Paths?\*\*:\s*(.+)$/i);
|
|
53
|
+
if (pathsMatch) {
|
|
54
|
+
const pathString = pathsMatch[1];
|
|
55
|
+
const backtickPatterns = pathString.match(/`([^`]+)`/g);
|
|
56
|
+
if (backtickPatterns) {
|
|
57
|
+
paths = backtickPatterns.map(p => p.replace(/`/g, '').trim());
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
paths = pathString.split(',').map(p => p.trim());
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Match **Severity**: critical|high|medium|low
|
|
65
|
+
const severityMatch = trimmed.match(/^\*\*Severity\*\*:\s*(critical|high|medium|low)/i);
|
|
66
|
+
if (severityMatch) {
|
|
67
|
+
severity = severityMatch[1].toLowerCase();
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Match **Reason**: ...
|
|
71
|
+
const reasonMatch = trimmed.match(/^\*\*Reason\*\*:\s*(.+)$/i);
|
|
72
|
+
if (reasonMatch) {
|
|
73
|
+
reason = reasonMatch[1].trim();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Only add if we have at least paths and reason
|
|
78
|
+
if (paths.length > 0 && reason) {
|
|
79
|
+
policies.push({ name, paths, reason, severity });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { policies, instructions };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Serialize ReviewPolicy objects and instructions back to markdown
|
|
86
|
+
*/
|
|
87
|
+
function serializePolicies(policies, instructions = []) {
|
|
88
|
+
const lines = ['# Review Policies', ''];
|
|
89
|
+
if (instructions.length > 0) {
|
|
90
|
+
lines.push('## Instructions', '');
|
|
91
|
+
for (const instruction of instructions) {
|
|
92
|
+
lines.push(`- ${instruction}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push('');
|
|
95
|
+
}
|
|
96
|
+
for (const policy of policies) {
|
|
97
|
+
lines.push(`## ${policy.name}`);
|
|
98
|
+
lines.push(`- **Paths**: ${policy.paths.map(p => '`' + p + '`').join(', ')}`);
|
|
99
|
+
lines.push(`- **Severity**: ${policy.severity}`);
|
|
100
|
+
lines.push(`- **Reason**: ${policy.reason}`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Severity Colors
|
|
107
|
+
// ============================================================================
|
|
108
|
+
function severityColor(severity) {
|
|
109
|
+
switch (severity) {
|
|
110
|
+
case 'critical': return chalk.red;
|
|
111
|
+
case 'high': return chalk.yellow;
|
|
112
|
+
case 'medium': return chalk.blue;
|
|
113
|
+
case 'low': return chalk.dim;
|
|
114
|
+
default: return chalk.white;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// Commands
|
|
119
|
+
// ============================================================================
|
|
120
|
+
/**
|
|
121
|
+
* List all review policies
|
|
122
|
+
*/
|
|
123
|
+
export async function listPolicies() {
|
|
124
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
125
|
+
if (!existsSync(filePath)) {
|
|
126
|
+
console.log(chalk.yellow('\nNo review-policy.md found.'));
|
|
127
|
+
console.log(chalk.dim('Run "haystack policy init" to create one.\n'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
131
|
+
const { policies, instructions } = parsePolicyMarkdown(content);
|
|
132
|
+
if (policies.length === 0 && instructions.length === 0) {
|
|
133
|
+
console.log(chalk.yellow('\nNo policies or instructions defined in review-policy.md'));
|
|
134
|
+
console.log(chalk.dim('Run "haystack policy add" to create one.\n'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (instructions.length > 0) {
|
|
138
|
+
console.log(chalk.cyan('\n📝 Review Instructions\n'));
|
|
139
|
+
for (const instruction of instructions) {
|
|
140
|
+
console.log(` ${chalk.dim('•')} ${instruction}`);
|
|
141
|
+
}
|
|
142
|
+
console.log('');
|
|
143
|
+
}
|
|
144
|
+
if (policies.length > 0) {
|
|
145
|
+
console.log(chalk.cyan('📋 Review Policies\n'));
|
|
146
|
+
for (const policy of policies) {
|
|
147
|
+
const color = severityColor(policy.severity);
|
|
148
|
+
console.log(` ${chalk.bold(policy.name)}`);
|
|
149
|
+
console.log(` Severity: ${color(policy.severity)}`);
|
|
150
|
+
console.log(` Paths: ${chalk.dim(policy.paths.join(', '))}`);
|
|
151
|
+
console.log(` Reason: ${policy.reason}`);
|
|
152
|
+
console.log('');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Add a new review policy interactively
|
|
158
|
+
*/
|
|
159
|
+
export async function addPolicy(nameArg) {
|
|
160
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
161
|
+
// Load existing policies and instructions
|
|
162
|
+
let policies = [];
|
|
163
|
+
let instructions = [];
|
|
164
|
+
if (existsSync(filePath)) {
|
|
165
|
+
const existingContent = readFileSync(filePath, 'utf-8');
|
|
166
|
+
const parsed = parsePolicyMarkdown(existingContent);
|
|
167
|
+
policies = parsed.policies;
|
|
168
|
+
instructions = parsed.instructions;
|
|
169
|
+
}
|
|
170
|
+
console.log(chalk.cyan('\n➕ Add Review Policy\n'));
|
|
171
|
+
// Prompt for policy details
|
|
172
|
+
const answers = await inquirer.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'input',
|
|
175
|
+
name: 'name',
|
|
176
|
+
message: 'Policy name:',
|
|
177
|
+
default: nameArg,
|
|
178
|
+
validate: (input) => {
|
|
179
|
+
if (!input.trim())
|
|
180
|
+
return 'Name is required';
|
|
181
|
+
if (policies.some(p => p.name.toLowerCase() === input.toLowerCase())) {
|
|
182
|
+
return 'A policy with this name already exists';
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
type: 'input',
|
|
189
|
+
name: 'paths',
|
|
190
|
+
message: 'File patterns (comma-separated globs):',
|
|
191
|
+
validate: (input) => input.trim() ? true : 'At least one path pattern is required',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: 'list',
|
|
195
|
+
name: 'severity',
|
|
196
|
+
message: 'Severity:',
|
|
197
|
+
choices: [
|
|
198
|
+
{ name: chalk.red('critical') + ' - Always requires human review', value: 'critical' },
|
|
199
|
+
{ name: chalk.yellow('high') + ' - Usually requires review', value: 'high' },
|
|
200
|
+
{ name: chalk.blue('medium') + ' - Should be reviewed', value: 'medium' },
|
|
201
|
+
{ name: chalk.dim('low') + ' - Nice to review', value: 'low' },
|
|
202
|
+
],
|
|
203
|
+
default: 'medium',
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: 'input',
|
|
207
|
+
name: 'reason',
|
|
208
|
+
message: 'Reason for requiring review:',
|
|
209
|
+
validate: (input) => input.trim() ? true : 'Reason is required',
|
|
210
|
+
},
|
|
211
|
+
]);
|
|
212
|
+
// Parse paths from comma-separated input
|
|
213
|
+
const paths = answers.paths.split(',').map((p) => p.trim()).filter(Boolean);
|
|
214
|
+
const newPolicy = {
|
|
215
|
+
name: answers.name.trim(),
|
|
216
|
+
paths,
|
|
217
|
+
severity: answers.severity,
|
|
218
|
+
reason: answers.reason.trim(),
|
|
219
|
+
};
|
|
220
|
+
policies.push(newPolicy);
|
|
221
|
+
// Ensure directory exists
|
|
222
|
+
const dir = dirname(filePath);
|
|
223
|
+
if (!existsSync(dir)) {
|
|
224
|
+
mkdirSync(dir, { recursive: true });
|
|
225
|
+
}
|
|
226
|
+
// Write updated file
|
|
227
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
228
|
+
console.log(chalk.green(`\n✓ Added policy "${newPolicy.name}"`));
|
|
229
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Remove a policy by name
|
|
233
|
+
*/
|
|
234
|
+
export async function removePolicy(name) {
|
|
235
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
236
|
+
if (!existsSync(filePath)) {
|
|
237
|
+
console.log(chalk.red('\nNo review-policy.md found.\n'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
241
|
+
const parsed = parsePolicyMarkdown(content);
|
|
242
|
+
let policies = parsed.policies;
|
|
243
|
+
const instructions = parsed.instructions;
|
|
244
|
+
// Find policy (case-insensitive)
|
|
245
|
+
const index = policies.findIndex(p => p.name.toLowerCase() === name.toLowerCase());
|
|
246
|
+
if (index === -1) {
|
|
247
|
+
console.log(chalk.red(`\nPolicy "${name}" not found.\n`));
|
|
248
|
+
console.log(chalk.dim('Available policies:'));
|
|
249
|
+
for (const p of policies) {
|
|
250
|
+
console.log(chalk.dim(` - ${p.name}`));
|
|
251
|
+
}
|
|
252
|
+
console.log('');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const removed = policies[index];
|
|
256
|
+
// Confirm removal
|
|
257
|
+
const { confirm } = await inquirer.prompt([
|
|
258
|
+
{
|
|
259
|
+
type: 'confirm',
|
|
260
|
+
name: 'confirm',
|
|
261
|
+
message: `Remove policy "${removed.name}"?`,
|
|
262
|
+
default: false,
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
if (!confirm) {
|
|
266
|
+
console.log(chalk.dim('\nCancelled.\n'));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// Remove and write
|
|
270
|
+
policies = policies.filter((_, i) => i !== index);
|
|
271
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
272
|
+
console.log(chalk.green(`\n✓ Removed policy "${removed.name}"`));
|
|
273
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Add a review instruction
|
|
277
|
+
*/
|
|
278
|
+
export async function addInstruction(text) {
|
|
279
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
280
|
+
// Load existing
|
|
281
|
+
let policies = [];
|
|
282
|
+
let instructions = [];
|
|
283
|
+
if (existsSync(filePath)) {
|
|
284
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
285
|
+
const parsed = parsePolicyMarkdown(content);
|
|
286
|
+
policies = parsed.policies;
|
|
287
|
+
instructions = parsed.instructions;
|
|
288
|
+
}
|
|
289
|
+
let instruction = text?.trim();
|
|
290
|
+
if (!instruction) {
|
|
291
|
+
const answers = await inquirer.prompt([
|
|
292
|
+
{
|
|
293
|
+
type: 'input',
|
|
294
|
+
name: 'instruction',
|
|
295
|
+
message: 'Review instruction:',
|
|
296
|
+
validate: (input) => input.trim() ? true : 'Instruction text is required',
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
instruction = answers.instruction.trim();
|
|
300
|
+
}
|
|
301
|
+
instructions.push(instruction);
|
|
302
|
+
// Ensure directory exists
|
|
303
|
+
const dir = dirname(filePath);
|
|
304
|
+
if (!existsSync(dir)) {
|
|
305
|
+
mkdirSync(dir, { recursive: true });
|
|
306
|
+
}
|
|
307
|
+
writeFileSync(filePath, serializePolicies(policies, instructions));
|
|
308
|
+
console.log(chalk.green(`\n✓ Added instruction: "${instruction}"`));
|
|
309
|
+
console.log(chalk.dim(` Updated ${POLICY_PATH}\n`));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Create initial review-policy.md with example policies
|
|
313
|
+
*/
|
|
314
|
+
export async function initPolicies(options) {
|
|
315
|
+
const filePath = join(process.cwd(), POLICY_PATH);
|
|
316
|
+
if (existsSync(filePath) && !options.force) {
|
|
317
|
+
console.log(chalk.yellow('\nreview-policy.md already exists. Use --force to overwrite.\n'));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Default template policies
|
|
321
|
+
const defaultPolicies = [
|
|
322
|
+
{
|
|
323
|
+
name: 'Infrastructure changes',
|
|
324
|
+
paths: ['terraform/**', '*.tf', 'pulumi/**', 'cdk/**'],
|
|
325
|
+
severity: 'high',
|
|
326
|
+
reason: 'Infrastructure changes require manual review for security and cost implications',
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: 'Secret files',
|
|
330
|
+
paths: ['**/*.secret*', '**/*.env*', '**/credentials*', '**/*.pem', '**/*.key'],
|
|
331
|
+
severity: 'critical',
|
|
332
|
+
reason: 'Files potentially containing secrets require security review',
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: 'CI/CD pipelines',
|
|
336
|
+
paths: ['.github/workflows/**', '.gitlab-ci.yml', 'Jenkinsfile', '.circleci/**'],
|
|
337
|
+
severity: 'high',
|
|
338
|
+
reason: 'CI/CD changes can affect deployment security and reliability',
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: 'Weak test coverage',
|
|
342
|
+
paths: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'],
|
|
343
|
+
severity: 'medium',
|
|
344
|
+
reason: 'Non-trivial logic changes without test coverage should be reviewed by a human to verify correctness',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: 'Unverified agent changes',
|
|
348
|
+
paths: ['**/*'],
|
|
349
|
+
severity: 'medium',
|
|
350
|
+
reason: 'Agent-authored PRs without verification evidence (tests run, build checked, UI verified) should be reviewed by a human',
|
|
351
|
+
},
|
|
352
|
+
];
|
|
353
|
+
// Ensure directory exists
|
|
354
|
+
const dir = dirname(filePath);
|
|
355
|
+
if (!existsSync(dir)) {
|
|
356
|
+
mkdirSync(dir, { recursive: true });
|
|
357
|
+
}
|
|
358
|
+
writeFileSync(filePath, serializePolicies(defaultPolicies));
|
|
359
|
+
console.log(chalk.green(`\n✓ Created ${POLICY_PATH}`));
|
|
360
|
+
console.log(chalk.dim('\nDefault policies added:'));
|
|
361
|
+
for (const p of defaultPolicies) {
|
|
362
|
+
console.log(chalk.dim(` - ${p.name} (${p.severity})`));
|
|
363
|
+
}
|
|
364
|
+
console.log(chalk.dim('\nEdit the file or use "haystack policy add" / "haystack policy add-instruction" to customize.\n'));
|
|
365
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pr-status command — Show the current Haystack status of a PR.
|
|
3
|
+
*
|
|
4
|
+
* Calls the classify-debug endpoint to determine which inbox bucket the PR
|
|
5
|
+
* is in (analyzing, auto-fixing, good-to-merge, issues, needs-assignment, etc.)
|
|
6
|
+
* and displays a human-readable summary.
|
|
7
|
+
*
|
|
8
|
+
* Accepts a PR identifier in several formats:
|
|
9
|
+
* 123 # Uses current repo's owner/repo
|
|
10
|
+
* owner/repo#123 # Fully qualified
|
|
11
|
+
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
12
|
+
*/
|
|
13
|
+
export interface PRStatusOptions {
|
|
14
|
+
json?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function prStatusCommand(identifier: string, options: PRStatusOptions): Promise<void>;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pr-status command — Show the current Haystack status of a PR.
|
|
3
|
+
*
|
|
4
|
+
* Calls the classify-debug endpoint to determine which inbox bucket the PR
|
|
5
|
+
* is in (analyzing, auto-fixing, good-to-merge, issues, needs-assignment, etc.)
|
|
6
|
+
* and displays a human-readable summary.
|
|
7
|
+
*
|
|
8
|
+
* Accepts a PR identifier in several formats:
|
|
9
|
+
* 123 # Uses current repo's owner/repo
|
|
10
|
+
* owner/repo#123 # Fully qualified
|
|
11
|
+
* https://github.com/owner/repo/pull/123 # GitHub URL
|
|
12
|
+
*/
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { loadToken } from './login.js';
|
|
15
|
+
import { parseRemoteUrl } from '../utils/git.js';
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// PR identifier parsing
|
|
18
|
+
// ============================================================================
|
|
19
|
+
function parsePRIdentifier(identifier) {
|
|
20
|
+
const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
21
|
+
if (urlMatch) {
|
|
22
|
+
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
|
|
23
|
+
}
|
|
24
|
+
const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
25
|
+
if (qualifiedMatch) {
|
|
26
|
+
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
|
|
27
|
+
}
|
|
28
|
+
const numberMatch = identifier.match(/^#?(\d+)$/);
|
|
29
|
+
if (numberMatch) {
|
|
30
|
+
const prNumber = parseInt(numberMatch[1], 10);
|
|
31
|
+
try {
|
|
32
|
+
const remote = parseRemoteUrl('origin');
|
|
33
|
+
return { owner: remote.owner, repo: remote.repo, prNumber };
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
|
|
37
|
+
`Use the full format: haystack pr-status owner/repo#${prNumber}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
|
|
41
|
+
`Accepted formats:\n` +
|
|
42
|
+
` haystack pr-status 123\n` +
|
|
43
|
+
` haystack pr-status owner/repo#123\n` +
|
|
44
|
+
` haystack pr-status https://github.com/owner/repo/pull/123`);
|
|
45
|
+
}
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Display helpers
|
|
48
|
+
// ============================================================================
|
|
49
|
+
const BUCKET_DISPLAY = {
|
|
50
|
+
'analyzing': { label: 'Analyzing', color: chalk.blue, icon: '...' },
|
|
51
|
+
'auto-fixing': { label: 'Auto-fixing', color: chalk.blue, icon: '>>>' },
|
|
52
|
+
'good-to-merge': { label: 'Good to Merge', color: chalk.green, icon: '[+]' },
|
|
53
|
+
'failed-when-merging': { label: 'Failed When Merging', color: chalk.red, icon: '[!]' },
|
|
54
|
+
'issues': { label: 'Issues Found', color: chalk.yellow, icon: '[?]' },
|
|
55
|
+
'needs-assignment': { label: 'Needs Reviewer Assignment', color: chalk.yellow, icon: '[?]' },
|
|
56
|
+
'needs-shepherding': { label: 'Needs Manual Merge', color: chalk.yellow, icon: '[!]' },
|
|
57
|
+
'needs-fixing': { label: 'Agent Fixing CI', color: chalk.blue, icon: '>>>' },
|
|
58
|
+
'awaiting-review': { label: 'Awaiting Review', color: chalk.cyan, icon: '[-]' },
|
|
59
|
+
'review-requested': { label: 'Review Requested', color: chalk.cyan, icon: '[-]' },
|
|
60
|
+
'snoozed': { label: 'Snoozed', color: chalk.dim, icon: '[-]' },
|
|
61
|
+
};
|
|
62
|
+
function formatBucket(bucket) {
|
|
63
|
+
if (!bucket)
|
|
64
|
+
return chalk.dim('Not classified (PR may be closed, or not yet analyzed)');
|
|
65
|
+
const info = BUCKET_DISPLAY[bucket];
|
|
66
|
+
if (!info)
|
|
67
|
+
return chalk.dim(bucket);
|
|
68
|
+
return `${info.icon} ${info.color(info.label)}`;
|
|
69
|
+
}
|
|
70
|
+
function formatAnalysisStatus(status) {
|
|
71
|
+
switch (status) {
|
|
72
|
+
case 'ready': return chalk.green('Ready');
|
|
73
|
+
case 'in_queue': return chalk.blue('In queue');
|
|
74
|
+
case 'pending': return chalk.dim('Pending');
|
|
75
|
+
case 'error': return chalk.red('Error');
|
|
76
|
+
default: return chalk.dim(status ?? 'unknown');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function formatVerdict(verdict) {
|
|
80
|
+
switch (verdict) {
|
|
81
|
+
case 'clean': return chalk.green('Clean');
|
|
82
|
+
case 'has-issues': return chalk.yellow('Has issues');
|
|
83
|
+
case 'needs-review': return chalk.cyan('Needs review');
|
|
84
|
+
default: return chalk.dim(verdict ?? 'n/a');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// API call
|
|
89
|
+
// ============================================================================
|
|
90
|
+
const HAYSTACK_API = 'https://haystackeditor.com';
|
|
91
|
+
async function fetchClassification(owner, repo, prNumber, token) {
|
|
92
|
+
const prId = `${owner}/${repo}#${prNumber}`;
|
|
93
|
+
const response = await fetch(`${HAYSTACK_API}/auth/classify-debug?pr=${encodeURIComponent(prId)}`, {
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${token}`,
|
|
96
|
+
'User-Agent': 'Haystack-CLI',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
if (response.status === 401) {
|
|
101
|
+
throw new Error('Authentication failed. Run `haystack login` to re-authenticate.');
|
|
102
|
+
}
|
|
103
|
+
if (response.status === 403) {
|
|
104
|
+
throw new Error('Access denied — you may not have permission to this repository.');
|
|
105
|
+
}
|
|
106
|
+
const body = await response.text().catch(() => '');
|
|
107
|
+
throw new Error(`API error (${response.status}): ${body}`);
|
|
108
|
+
}
|
|
109
|
+
return response.json();
|
|
110
|
+
}
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Command
|
|
113
|
+
// ============================================================================
|
|
114
|
+
export async function prStatusCommand(identifier, options) {
|
|
115
|
+
let pr;
|
|
116
|
+
try {
|
|
117
|
+
pr = parsePRIdentifier(identifier);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
const token = await loadToken();
|
|
124
|
+
if (!token) {
|
|
125
|
+
console.error(chalk.red('\nNot logged in. Run `haystack login` first.\n'));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
129
|
+
if (!options.json) {
|
|
130
|
+
console.log(chalk.dim(`\nFetching status for ${prLabel}...\n`));
|
|
131
|
+
}
|
|
132
|
+
let data;
|
|
133
|
+
try {
|
|
134
|
+
data = await fetchClassification(pr.owner, pr.repo, pr.prNumber, token);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
// JSON mode — pure JSON to stdout, nothing else
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(data, null, 2));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Human-readable output
|
|
146
|
+
console.log(` ${chalk.bold('PR')}: ${data.prId}`);
|
|
147
|
+
console.log(` ${chalk.bold('Status')}: ${formatBucket(data.bucket)}`);
|
|
148
|
+
console.log(` ${chalk.bold('Analysis')}: ${formatAnalysisStatus(data.inputs.analysisStatus)}`);
|
|
149
|
+
if (data.inputs.analysisVerdict) {
|
|
150
|
+
console.log(` ${chalk.bold('Verdict')}: ${formatVerdict(data.inputs.analysisVerdict)}`);
|
|
151
|
+
}
|
|
152
|
+
if (data.inputs.haystackRating != null) {
|
|
153
|
+
console.log(` ${chalk.bold('Rating')}: ${data.inputs.haystackRating}/5`);
|
|
154
|
+
}
|
|
155
|
+
console.log(` ${chalk.bold('Reason')}: ${chalk.dim(data.reason)}`);
|
|
156
|
+
// Extra context
|
|
157
|
+
if (!data.inputs.isOpen) {
|
|
158
|
+
console.log(chalk.dim(`\n PR is closed or merged.`));
|
|
159
|
+
}
|
|
160
|
+
if (!data.inputs.isAuthor) {
|
|
161
|
+
console.log(chalk.dim(`\n You are not the author (author: ${data.inputs.author}).`));
|
|
162
|
+
}
|
|
163
|
+
// Labels of interest
|
|
164
|
+
const haystackLabels = (data.inputs.labels || []).filter(l => l.startsWith('haystack:'));
|
|
165
|
+
if (haystackLabels.length > 0) {
|
|
166
|
+
console.log(` ${chalk.bold('Labels')}: ${haystackLabels.join(', ')}`);
|
|
167
|
+
}
|
|
168
|
+
// User overrides
|
|
169
|
+
const overrides = [];
|
|
170
|
+
if (data.inputs.dismissed)
|
|
171
|
+
overrides.push('dismissed');
|
|
172
|
+
if (data.inputs.dismissedAgentAlerts)
|
|
173
|
+
overrides.push('agent alerts dismissed');
|
|
174
|
+
if (data.inputs.dismissedNeedsAssignment)
|
|
175
|
+
overrides.push('needs-assignment dismissed');
|
|
176
|
+
if (data.inputs.manuallyGoodToMerge)
|
|
177
|
+
overrides.push('manually marked good-to-merge');
|
|
178
|
+
if (data.inputs.snoozed)
|
|
179
|
+
overrides.push('snoozed');
|
|
180
|
+
if (overrides.length > 0) {
|
|
181
|
+
console.log(` ${chalk.bold('Overrides')}: ${overrides.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
// Stale warning
|
|
184
|
+
if (data.staleWarning) {
|
|
185
|
+
console.log(chalk.yellow(`\n Warning: ${data.staleWarning}`));
|
|
186
|
+
}
|
|
187
|
+
console.log('');
|
|
188
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* haystack setup - Interactive onboarding wizard
|
|
3
|
+
*
|
|
4
|
+
* CLI equivalent of the web onboarding wizard. Walks through:
|
|
5
|
+
* 1. Login check
|
|
6
|
+
* 2. Repository selection
|
|
7
|
+
* 3. Scan for coding rules
|
|
8
|
+
* 4. Scan for wait-for signals (CI checks, bot reviews)
|
|
9
|
+
* 5. Scan for review policies
|
|
10
|
+
* 6. Review discovered items (toggle on/off)
|
|
11
|
+
* 7. Confirm & write .haystack.json to selected repos
|
|
12
|
+
*/
|
|
13
|
+
export declare function setupCommand(): Promise<void>;
|