@haystackeditor/cli 0.8.1 → 0.9.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 +46 -0
- package/dist/assets/hooks/llm-rules-template.md +21 -0
- 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/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/skills.d.ts +2 -2
- package/dist/commands/skills.js +51 -186
- package/dist/commands/submit.d.ts +22 -0
- package/dist/commands/submit.js +428 -0
- package/dist/commands/triage.d.ts +16 -0
- package/dist/commands/triage.js +354 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +232 -2
- 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 +266 -0
- package/dist/triage/runner.d.ts +21 -0
- package/dist/triage/runner.js +325 -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 +38 -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 +4 -2
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* triage command — View Haystack analysis results for any PR.
|
|
3
|
+
*
|
|
4
|
+
* Fetches the full rating synthesis (same data shown on the Haystack feed)
|
|
5
|
+
* and displays verdict, findings, verified bugs, and agent fix prompts.
|
|
6
|
+
*
|
|
7
|
+
* Accepts a PR identifier in several formats:
|
|
8
|
+
* haystack triage 123 # Uses current repo's owner/repo
|
|
9
|
+
* haystack triage owner/repo#123 # Fully qualified
|
|
10
|
+
* haystack triage https://github.com/owner/repo/pull/123
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { checkAnalysisReady, fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
14
|
+
import { parseRemoteUrl } from '../utils/git.js';
|
|
15
|
+
import { loadToken } from './login.js';
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// PR identifier parsing
|
|
18
|
+
// ============================================================================
|
|
19
|
+
/**
|
|
20
|
+
* Parse a PR identifier from various formats:
|
|
21
|
+
* 123 -> uses current repo
|
|
22
|
+
* #123 -> uses current repo
|
|
23
|
+
* owner/repo#123 -> fully qualified
|
|
24
|
+
* https://github.com/owner/repo/pull/123 -> GitHub URL
|
|
25
|
+
*/
|
|
26
|
+
function parsePRIdentifier(identifier) {
|
|
27
|
+
// GitHub URL: https://github.com/owner/repo/pull/123
|
|
28
|
+
const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
|
|
29
|
+
if (urlMatch) {
|
|
30
|
+
return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
|
|
31
|
+
}
|
|
32
|
+
// Fully qualified: owner/repo#123
|
|
33
|
+
const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
34
|
+
if (qualifiedMatch) {
|
|
35
|
+
return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
|
|
36
|
+
}
|
|
37
|
+
// Bare number: 123 or #123
|
|
38
|
+
const numberMatch = identifier.match(/^#?(\d+)$/);
|
|
39
|
+
if (numberMatch) {
|
|
40
|
+
const prNumber = parseInt(numberMatch[1], 10);
|
|
41
|
+
// Infer owner/repo from git remote
|
|
42
|
+
try {
|
|
43
|
+
const remote = parseRemoteUrl('origin');
|
|
44
|
+
return { owner: remote.owner, repo: remote.repo, prNumber };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
|
|
48
|
+
`Use the full format: haystack triage owner/repo#${prNumber}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
|
|
52
|
+
`Accepted formats:\n` +
|
|
53
|
+
` haystack triage 123\n` +
|
|
54
|
+
` haystack triage owner/repo#123\n` +
|
|
55
|
+
` haystack triage https://github.com/owner/repo/pull/123`);
|
|
56
|
+
}
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Output formatting
|
|
59
|
+
// ============================================================================
|
|
60
|
+
function ratingLabel(rating) {
|
|
61
|
+
switch (rating) {
|
|
62
|
+
case 5: return chalk.green.bold('5/5') + chalk.green(' — Safe to merge');
|
|
63
|
+
case 4: return chalk.green('4/5') + chalk.dim(' — Low risk');
|
|
64
|
+
case 3: return chalk.yellow('3/5') + chalk.dim(' — Moderate risk');
|
|
65
|
+
case 2: return chalk.red('2/5') + chalk.dim(' — High risk');
|
|
66
|
+
case 1: return chalk.red.bold('1/5') + chalk.red(' — Critical issues');
|
|
67
|
+
default: return chalk.dim(`${rating}/5`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function verdictLabel(verdict) {
|
|
71
|
+
switch (verdict) {
|
|
72
|
+
case 'clean': return chalk.green.bold('Clean');
|
|
73
|
+
case 'has-issues': return chalk.yellow.bold('Has Issues');
|
|
74
|
+
default: return chalk.dim('Unknown');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function severityIcon(severity) {
|
|
78
|
+
switch (severity) {
|
|
79
|
+
case 'critical': return chalk.red.bold('✗✗');
|
|
80
|
+
case 'high': return chalk.red('✗');
|
|
81
|
+
case 'medium': return chalk.yellow('!');
|
|
82
|
+
case 'low': return chalk.blue('·');
|
|
83
|
+
default: return chalk.dim('-');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function printRichOutput(pr, synthesis) {
|
|
87
|
+
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
88
|
+
console.log(chalk.bold('\n' + '━'.repeat(60)));
|
|
89
|
+
console.log(chalk.bold(` Haystack Analysis: ${pr.owner}/${pr.repo}#${pr.prNumber}`));
|
|
90
|
+
console.log(chalk.bold('━'.repeat(60) + '\n'));
|
|
91
|
+
// Rating & verdict
|
|
92
|
+
console.log(` ${chalk.dim('Rating:')} ${ratingLabel(synthesis.haystackRating)}`);
|
|
93
|
+
console.log(` ${chalk.dim('Verdict:')} ${verdictLabel(synthesis.analysisVerdict)}`);
|
|
94
|
+
if (synthesis.phase) {
|
|
95
|
+
const phaseLabel = synthesis.phase === 2 ? 'Final (post-verification)' : 'Preliminary (pre-verification)';
|
|
96
|
+
console.log(` ${chalk.dim('Phase:')} ${chalk.dim(phaseLabel)}`);
|
|
97
|
+
}
|
|
98
|
+
if (synthesis.verificationConfidence) {
|
|
99
|
+
const conf = synthesis.verificationConfidence;
|
|
100
|
+
const levelColor = conf.level === 'high' ? chalk.green : conf.level === 'medium' ? chalk.yellow : chalk.red;
|
|
101
|
+
console.log(` ${chalk.dim('Verified:')} ${levelColor(conf.level)} — ${chalk.dim(conf.summary)}`);
|
|
102
|
+
}
|
|
103
|
+
if (synthesis.reviewRecommendation?.canAutoMerge) {
|
|
104
|
+
console.log(` ${chalk.dim('Merge:')} ${chalk.green('Safe to auto-merge')}`);
|
|
105
|
+
}
|
|
106
|
+
// Verification results (build/test/lint)
|
|
107
|
+
if (synthesis.verification) {
|
|
108
|
+
const v = synthesis.verification;
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (v.build)
|
|
111
|
+
parts.push(v.build.success ? chalk.green('build ✓') : chalk.red('build ✗'));
|
|
112
|
+
if (v.test)
|
|
113
|
+
parts.push(v.test.success ? chalk.green('test ✓') : chalk.red('test ✗'));
|
|
114
|
+
if (v.lint)
|
|
115
|
+
parts.push(v.lint.success ? chalk.green('lint ✓') : chalk.red('lint ✗'));
|
|
116
|
+
if (parts.length > 0) {
|
|
117
|
+
console.log(` ${chalk.dim('Checks:')} ${parts.join(' ')}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Findings (synthesisDisplay) — the main actionable content
|
|
121
|
+
if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0) {
|
|
122
|
+
console.log(chalk.bold('\n Findings\n'));
|
|
123
|
+
for (let i = 0; i < synthesis.synthesisDisplay.length; i++) {
|
|
124
|
+
const finding = synthesis.synthesisDisplay[i];
|
|
125
|
+
const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
|
|
126
|
+
console.log(` ${chalk.yellow(`${i + 1}.`)} ${chalk.bold(finding.category)}${sourceTag}`);
|
|
127
|
+
console.log(` ${finding.summary}`);
|
|
128
|
+
if (finding.detail) {
|
|
129
|
+
// Indent multi-line detail
|
|
130
|
+
const detailLines = finding.detail.split('\n');
|
|
131
|
+
for (const line of detailLines) {
|
|
132
|
+
console.log(` ${chalk.dim(line)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Verified bugs
|
|
139
|
+
const confirmedBugs = (synthesis.verifiedBugs || []).filter(b => b.verified);
|
|
140
|
+
if (confirmedBugs.length > 0) {
|
|
141
|
+
console.log(chalk.bold(' Verified Bugs\n'));
|
|
142
|
+
for (const bug of confirmedBugs) {
|
|
143
|
+
const sourceTag = bug.source ? chalk.dim(` (via ${bug.source.type}${bug.source.author ? `: ${bug.source.author}` : ''})`) : '';
|
|
144
|
+
console.log(` ${severityIcon(bug.assessedSeverity)} ${chalk.bold(bug.originalTitle)}${sourceTag}`);
|
|
145
|
+
console.log(` ${chalk.dim('Severity:')} ${bug.assessedSeverity}${bug.likelihood ? `, ${bug.likelihood} occurrence` : ''}`);
|
|
146
|
+
console.log(` ${bug.rationale}`);
|
|
147
|
+
console.log('');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Human review reasons
|
|
151
|
+
if (synthesis.humanReviewReasons && synthesis.humanReviewReasons.length > 0) {
|
|
152
|
+
console.log(chalk.bold(' Human Review Required\n'));
|
|
153
|
+
for (const reason of synthesis.humanReviewReasons) {
|
|
154
|
+
const icon = reason.severity === 'high' || reason.severity === 'critical'
|
|
155
|
+
? chalk.red('!')
|
|
156
|
+
: chalk.yellow('·');
|
|
157
|
+
console.log(` ${icon} ${reason.reason}`);
|
|
158
|
+
if (reason.source) {
|
|
159
|
+
console.log(` ${chalk.dim(`Source: ${reason.source}`)}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
// Agent fix prompts — the key actionable output for coding agents
|
|
165
|
+
const fixableFindings = (synthesis.synthesisDisplay || []).filter(f => f.agentFixPrompt);
|
|
166
|
+
if (fixableFindings.length > 0) {
|
|
167
|
+
console.log(chalk.bold(' Agent Fix Prompts'));
|
|
168
|
+
console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
|
|
169
|
+
for (let i = 0; i < fixableFindings.length; i++) {
|
|
170
|
+
const finding = fixableFindings[i];
|
|
171
|
+
console.log(chalk.cyan(` --- Fix ${i + 1}: ${finding.category} ---`));
|
|
172
|
+
console.log(` ${finding.agentFixPrompt}`);
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Verification impact
|
|
177
|
+
if (synthesis.verificationImpact) {
|
|
178
|
+
console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
console.log(` ${chalk.dim('Full review:')} ${chalk.cyan(reviewUrl)}`);
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
function printJsonOutput(pr, synthesis) {
|
|
185
|
+
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
186
|
+
const output = {
|
|
187
|
+
owner: pr.owner,
|
|
188
|
+
repo: pr.repo,
|
|
189
|
+
prNumber: pr.prNumber,
|
|
190
|
+
haystackRating: synthesis.haystackRating,
|
|
191
|
+
analysisVerdict: synthesis.analysisVerdict,
|
|
192
|
+
phase: synthesis.phase,
|
|
193
|
+
canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
|
|
194
|
+
needsHumanReview: synthesis.needsHumanReview,
|
|
195
|
+
findings: (synthesis.synthesisDisplay || []).map(f => ({
|
|
196
|
+
category: f.category,
|
|
197
|
+
summary: f.summary,
|
|
198
|
+
detail: f.detail,
|
|
199
|
+
agentFixPrompt: f.agentFixPrompt || null,
|
|
200
|
+
source: f.source || null,
|
|
201
|
+
})),
|
|
202
|
+
verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
|
|
203
|
+
title: b.originalTitle,
|
|
204
|
+
severity: b.assessedSeverity,
|
|
205
|
+
likelihood: b.likelihood,
|
|
206
|
+
rationale: b.rationale,
|
|
207
|
+
source: b.source || null,
|
|
208
|
+
})),
|
|
209
|
+
humanReviewReasons: synthesis.humanReviewReasons || [],
|
|
210
|
+
verification: synthesis.verification || null,
|
|
211
|
+
verificationConfidence: synthesis.verificationConfidence || null,
|
|
212
|
+
reviewUrl,
|
|
213
|
+
};
|
|
214
|
+
console.log(JSON.stringify(output, null, 2));
|
|
215
|
+
}
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Polling
|
|
218
|
+
// ============================================================================
|
|
219
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
220
|
+
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
221
|
+
async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
|
|
222
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
223
|
+
let consecutiveErrors = 0;
|
|
224
|
+
while (Date.now() < deadline) {
|
|
225
|
+
const result = await checkAnalysisReady(owner, repo, prNumber, token);
|
|
226
|
+
if (result.status === 'ready')
|
|
227
|
+
return 'ready';
|
|
228
|
+
if (result.status === 'error') {
|
|
229
|
+
consecutiveErrors++;
|
|
230
|
+
if (consecutiveErrors >= 5)
|
|
231
|
+
return 'error';
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
consecutiveErrors = 0;
|
|
235
|
+
}
|
|
236
|
+
if (!quiet) {
|
|
237
|
+
process.stderr.write(`\r ${chalk.dim('Waiting for analysis to complete...')}${' '.repeat(5)}`);
|
|
238
|
+
}
|
|
239
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
|
|
240
|
+
}
|
|
241
|
+
return 'pending';
|
|
242
|
+
}
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// Main command
|
|
245
|
+
// ============================================================================
|
|
246
|
+
export async function triageCommand(identifier, options) {
|
|
247
|
+
// Parse PR identifier
|
|
248
|
+
let pr;
|
|
249
|
+
try {
|
|
250
|
+
pr = parsePRIdentifier(identifier);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
// Load GitHub token for private repo auth
|
|
257
|
+
const token = await loadToken() || undefined;
|
|
258
|
+
if (!options.json) {
|
|
259
|
+
console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
|
|
260
|
+
}
|
|
261
|
+
// Check if analysis is ready — resolve to a terminal state before fetching
|
|
262
|
+
let analysisReady = false;
|
|
263
|
+
const readyResult = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
|
|
264
|
+
if (readyResult.status === 'ready') {
|
|
265
|
+
analysisReady = true;
|
|
266
|
+
}
|
|
267
|
+
else if (readyResult.status === 'pending') {
|
|
268
|
+
if (options.wait === false) {
|
|
269
|
+
if (options.json) {
|
|
270
|
+
console.log(JSON.stringify({
|
|
271
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
272
|
+
}, null, 2));
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.log(chalk.yellow('\n Analysis is still in progress.'));
|
|
276
|
+
console.log(chalk.dim(' Re-run without --no-wait to poll until complete.\n'));
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
// Wait for analysis
|
|
281
|
+
if (!options.json) {
|
|
282
|
+
console.log(chalk.dim(' Analysis in progress, waiting...'));
|
|
283
|
+
}
|
|
284
|
+
const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, token, options.json);
|
|
285
|
+
if (!options.json) {
|
|
286
|
+
process.stderr.write('\r' + ' '.repeat(60) + '\r');
|
|
287
|
+
}
|
|
288
|
+
if (waitResult === 'ready') {
|
|
289
|
+
analysisReady = true;
|
|
290
|
+
}
|
|
291
|
+
else if (waitResult === 'pending') {
|
|
292
|
+
if (options.json) {
|
|
293
|
+
console.log(JSON.stringify({
|
|
294
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
295
|
+
}, null, 2));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
console.log(chalk.yellow('\n Timed out waiting for analysis.'));
|
|
299
|
+
console.log(chalk.dim(' Try again later: ') + chalk.cyan(`haystack triage ${pr.owner}/${pr.repo}#${pr.prNumber}\n`));
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// waitResult === 'error'
|
|
305
|
+
if (options.json) {
|
|
306
|
+
console.log(JSON.stringify({
|
|
307
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
|
|
308
|
+
}, null, 2));
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.error(chalk.red('\n Analysis check failed.\n'));
|
|
312
|
+
}
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// readyResult.status === 'error'
|
|
318
|
+
if (options.json) {
|
|
319
|
+
console.log(JSON.stringify({
|
|
320
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
321
|
+
status: 'error', message: readyResult.message,
|
|
322
|
+
}, null, 2));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
console.error(chalk.red(`\n Analysis check failed: ${readyResult.message}\n`));
|
|
326
|
+
}
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
if (!analysisReady)
|
|
330
|
+
return;
|
|
331
|
+
// Fetch the full rating synthesis
|
|
332
|
+
const synthesis = await fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token);
|
|
333
|
+
if (!synthesis) {
|
|
334
|
+
if (options.json) {
|
|
335
|
+
console.log(JSON.stringify({
|
|
336
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
337
|
+
status: 'error', message: 'Rating synthesis not available',
|
|
338
|
+
}, null, 2));
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
console.error(chalk.yellow('\n Analysis completed but rating synthesis is not available yet.'));
|
|
342
|
+
console.log(chalk.dim(' Try again in a moment, or view the full review:'));
|
|
343
|
+
console.log(chalk.cyan(` https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
|
|
344
|
+
}
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
// Output
|
|
348
|
+
if (options.json) {
|
|
349
|
+
printJsonOutput(pr, synthesis);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
printRichOutput(pr, synthesis);
|
|
353
|
+
}
|
|
354
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* npx @haystackeditor/cli init # Set up .haystack.json
|
|
10
10
|
* npx @haystackeditor/cli status # Check configuration
|
|
11
11
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
|
+
* npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
|
|
13
|
+
* npx @haystackeditor/cli check-pending # Check analysis status
|
|
14
|
+
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
12
15
|
* npx @haystackeditor/cli secrets list # List stored secrets
|
|
13
16
|
*/
|
|
14
17
|
export {};
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* npx @haystackeditor/cli init # Set up .haystack.json
|
|
10
10
|
* npx @haystackeditor/cli status # Check configuration
|
|
11
11
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
12
|
+
* npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
|
|
13
|
+
* npx @haystackeditor/cli check-pending # Check analysis status
|
|
14
|
+
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
12
15
|
* npx @haystackeditor/cli secrets list # List stored secrets
|
|
13
16
|
*/
|
|
14
17
|
import { Command } from 'commander';
|
|
@@ -16,14 +19,19 @@ import { statusCommand } from './commands/status.js';
|
|
|
16
19
|
import { initCommand } from './commands/init.js';
|
|
17
20
|
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
18
21
|
import { listSecrets, setSecret, getSecret, getSecretsForRepo, deleteSecret } from './commands/secrets.js';
|
|
19
|
-
import { handleSandbox, handleAgenticTool } from './commands/config.js';
|
|
22
|
+
import { handleSandbox, handleAgenticTool, handleAutoMerge, handleWaitForReviewers } from './commands/config.js';
|
|
20
23
|
import { installSkills, listSkills } from './commands/skills.js';
|
|
21
24
|
import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
|
|
25
|
+
import { submitCommand } from './commands/submit.js';
|
|
26
|
+
import { checkPendingCommand } from './commands/check-pending.js';
|
|
27
|
+
import { installSessionHooks, sessionHooksStatus } from './commands/install-session-hooks.js';
|
|
28
|
+
import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } from './commands/policy.js';
|
|
29
|
+
import { triageCommand } from './commands/triage.js';
|
|
22
30
|
const program = new Command();
|
|
23
31
|
program
|
|
24
32
|
.name('haystack')
|
|
25
33
|
.description('Set up Haystack verification for your project')
|
|
26
|
-
.version('0.
|
|
34
|
+
.version('0.9.0');
|
|
27
35
|
program
|
|
28
36
|
.command('init')
|
|
29
37
|
.description('Create .haystack.json configuration')
|
|
@@ -50,6 +58,107 @@ program
|
|
|
50
58
|
.command('logout')
|
|
51
59
|
.description('Remove stored credentials')
|
|
52
60
|
.action(logoutCommand);
|
|
61
|
+
program
|
|
62
|
+
.command('submit')
|
|
63
|
+
.description('Create a PR from current changes')
|
|
64
|
+
.option('--title <title>', 'PR title (default: last commit message)')
|
|
65
|
+
.option('--body <body>', 'PR body (default: commit messages since base)')
|
|
66
|
+
.option('--body-file <path>', 'Read PR body from file (use "-" for stdin)')
|
|
67
|
+
.option('--base <branch>', 'Base branch (default: main or master)')
|
|
68
|
+
.option('--draft', 'Create as draft PR')
|
|
69
|
+
.option('--review [reviewer]', 'Request human review (BLOCKS auto-merge — only use when explicitly asked)')
|
|
70
|
+
.option('--force', 'Skip pre-PR triage checks')
|
|
71
|
+
.option('--no-wait', 'Skip waiting for analysis results')
|
|
72
|
+
.addHelpText('after', `
|
|
73
|
+
This command is designed for AI coding agents to submit PRs.
|
|
74
|
+
|
|
75
|
+
1. Runs pre-PR triage (code review, rules, intent drift) via sub-agents
|
|
76
|
+
2. Pushes the current branch to origin
|
|
77
|
+
3. Creates a pull request on GitHub
|
|
78
|
+
4. Waits for Haystack analysis results (triggered via GitHub App webhook)
|
|
79
|
+
|
|
80
|
+
Pre-PR Triage:
|
|
81
|
+
Before creating the PR, haystack spawns parallel sub-agents to check for:
|
|
82
|
+
• Code review bugs (logic errors, null crashes, security issues)
|
|
83
|
+
• Rule violations (from .haystack/pr-rules.yml)
|
|
84
|
+
• Intent drift (AI agent deviations from user instructions)
|
|
85
|
+
|
|
86
|
+
Use --force to skip triage entirely.
|
|
87
|
+
Use --no-wait to skip waiting for analysis results.
|
|
88
|
+
|
|
89
|
+
Review Routing:
|
|
90
|
+
By default, PRs enter the auto-merge queue and merge automatically once
|
|
91
|
+
Haystack analysis passes. This is the correct default for most PRs.
|
|
92
|
+
|
|
93
|
+
⚠ Only use --review when the user EXPLICITLY asks for human review.
|
|
94
|
+
--review BLOCKS auto-merge — the PR will NOT merge until a human approves,
|
|
95
|
+
which delays merging. Do not add --review "just to be safe".
|
|
96
|
+
|
|
97
|
+
• --review Labels the PR "haystack:needs-review" for the
|
|
98
|
+
needs-assignment queue (a teammate will pick it up)
|
|
99
|
+
• --review <username> Same label, plus requests review from that GitHub user
|
|
100
|
+
|
|
101
|
+
Examples:
|
|
102
|
+
haystack submit # Triage, create PR, auto-merge queue
|
|
103
|
+
haystack submit --force # Skip triage
|
|
104
|
+
haystack submit --no-wait # Don't wait for analysis
|
|
105
|
+
haystack submit --title "Fix auth" # Custom PR title
|
|
106
|
+
haystack submit --body "## Summary" # Custom PR body (inline)
|
|
107
|
+
haystack submit --body-file body.md # Read body from file (supports markdown)
|
|
108
|
+
echo "## Summary" | haystack submit --body-file - # Read body from stdin
|
|
109
|
+
haystack submit --base develop # Target a different base branch
|
|
110
|
+
haystack submit --draft # Create as draft PR
|
|
111
|
+
haystack submit --review # ⚠ Blocks auto-merge, needs human approval
|
|
112
|
+
haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
|
|
113
|
+
`)
|
|
114
|
+
.action(submitCommand);
|
|
115
|
+
program
|
|
116
|
+
.command('check-pending')
|
|
117
|
+
.description('Check status of pending Haystack analysis')
|
|
118
|
+
.option('--json', 'Output as JSON')
|
|
119
|
+
.option('--hook', 'Minimal output for session-start hooks')
|
|
120
|
+
.option('--clear', 'Clear pending submit state')
|
|
121
|
+
.addHelpText('after', `
|
|
122
|
+
After \`haystack submit\`, this checks whether the analysis finished
|
|
123
|
+
and shows a two-state verdict:
|
|
124
|
+
|
|
125
|
+
Good to merge — No issues found, safe to merge
|
|
126
|
+
Needs your input — Bugs or rule violations found
|
|
127
|
+
|
|
128
|
+
This runs automatically on CLI session start if session hooks are installed.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
haystack check-pending # Rich terminal output
|
|
132
|
+
haystack check-pending --hook # Minimal output (for hooks)
|
|
133
|
+
haystack check-pending --json # Machine-readable JSON
|
|
134
|
+
haystack check-pending --clear # Clear pending state
|
|
135
|
+
`)
|
|
136
|
+
.action(checkPendingCommand);
|
|
137
|
+
program
|
|
138
|
+
.command('triage <pr>')
|
|
139
|
+
.description('View Haystack analysis results for a PR')
|
|
140
|
+
.option('--json', 'Output as JSON')
|
|
141
|
+
.option('--no-wait', 'Exit immediately if analysis is still pending')
|
|
142
|
+
.addHelpText('after', `
|
|
143
|
+
Look up triage results (bugs, rule violations, verdict) for any PR.
|
|
144
|
+
|
|
145
|
+
PR identifier formats:
|
|
146
|
+
123 PR number (uses current repo)
|
|
147
|
+
#123 PR number with hash
|
|
148
|
+
owner/repo#123 Fully qualified
|
|
149
|
+
https://github.com/owner/repo/pull/123 GitHub URL
|
|
150
|
+
|
|
151
|
+
By default, if analysis is still in progress, the command will poll
|
|
152
|
+
for up to 5 minutes. Use --no-wait to exit immediately instead.
|
|
153
|
+
|
|
154
|
+
Examples:
|
|
155
|
+
haystack triage 42 # Current repo, PR #42
|
|
156
|
+
haystack triage acme/widgets#99 # Specific repo
|
|
157
|
+
haystack triage https://github.com/o/r/pull/1 # From URL
|
|
158
|
+
haystack triage 42 --json # Machine-readable output
|
|
159
|
+
haystack triage 42 --no-wait # Don't wait if pending
|
|
160
|
+
`)
|
|
161
|
+
.action(triageCommand);
|
|
53
162
|
// Secrets subcommands
|
|
54
163
|
const secrets = program
|
|
55
164
|
.command('secrets')
|
|
@@ -123,6 +232,52 @@ Examples:
|
|
|
123
232
|
haystack config agentic-tool codex # Use your ChatGPT
|
|
124
233
|
`)
|
|
125
234
|
.action(handleAgenticTool);
|
|
235
|
+
config
|
|
236
|
+
.command('auto-merge [action]')
|
|
237
|
+
.description('Auto-merge safe PRs submitted via haystack submit (on|off|status)')
|
|
238
|
+
.addHelpText('after', `
|
|
239
|
+
Actions:
|
|
240
|
+
on, enable Enable auto-merge for safe PRs
|
|
241
|
+
off, disable Disable auto-merge
|
|
242
|
+
status Show current status (default)
|
|
243
|
+
|
|
244
|
+
When enabled, PRs submitted via \`haystack submit\` will be
|
|
245
|
+
automatically merged if Haystack analysis finds no issues
|
|
246
|
+
(safe to merge). PRs with bugs or rule violations still
|
|
247
|
+
require manual review.
|
|
248
|
+
|
|
249
|
+
Examples:
|
|
250
|
+
haystack config auto-merge # Show current status
|
|
251
|
+
haystack config auto-merge on # Enable auto-merge
|
|
252
|
+
haystack config auto-merge off # Disable auto-merge
|
|
253
|
+
`)
|
|
254
|
+
.action(handleAutoMerge);
|
|
255
|
+
config
|
|
256
|
+
.command('wait-for-reviewers [action] [reviewers...]')
|
|
257
|
+
.description('Configure which AI reviewers the merge queue waits for')
|
|
258
|
+
.addHelpText('after', `
|
|
259
|
+
Actions:
|
|
260
|
+
list Show current expected reviewers (default)
|
|
261
|
+
add <names> Add one or more AI reviewer sources
|
|
262
|
+
remove <names> Remove one or more AI reviewer sources
|
|
263
|
+
clear Remove all expected reviewers
|
|
264
|
+
|
|
265
|
+
AI reviewer sources are stored in .haystack.json under
|
|
266
|
+
babysitter.expected_ai_reviewers. The merge queue will
|
|
267
|
+
wait for these reviewers to post before starting the
|
|
268
|
+
grace period.
|
|
269
|
+
|
|
270
|
+
Run \`haystack config wait-for-reviewers list\` to see
|
|
271
|
+
all available reviewer sources and which are enabled.
|
|
272
|
+
|
|
273
|
+
Examples:
|
|
274
|
+
haystack config wait-for-reviewers # Show status
|
|
275
|
+
haystack config wait-for-reviewers add cursor # Wait for Cursor BugBot
|
|
276
|
+
haystack config wait-for-reviewers add cursor coderabbit # Add multiple
|
|
277
|
+
haystack config wait-for-reviewers remove cursor # Stop waiting for Cursor
|
|
278
|
+
haystack config wait-for-reviewers clear # Wait for none
|
|
279
|
+
`)
|
|
280
|
+
.action(handleWaitForReviewers);
|
|
126
281
|
// Skills subcommands
|
|
127
282
|
const skills = program
|
|
128
283
|
.command('skills')
|
|
@@ -186,6 +341,81 @@ hooks
|
|
|
186
341
|
.command('update')
|
|
187
342
|
.description('Update Entire CLI to the latest version')
|
|
188
343
|
.action(hooksUpdate);
|
|
344
|
+
hooks
|
|
345
|
+
.command('install-session')
|
|
346
|
+
.description('Install session-start hooks for coding CLIs')
|
|
347
|
+
.option('--cli <name>', 'Target CLI: claude, codex, gemini, or all')
|
|
348
|
+
.addHelpText('after', `
|
|
349
|
+
Configures your coding CLI to run \`haystack check-pending\` on session start.
|
|
350
|
+
This shows pending PR analysis results when you open a new terminal session.
|
|
351
|
+
|
|
352
|
+
Claude Code: Native SessionStart hook (.claude/settings.json)
|
|
353
|
+
Codex CLI: AGENTS.md instructions
|
|
354
|
+
Gemini CLI: GEMINI.md instructions
|
|
355
|
+
|
|
356
|
+
Examples:
|
|
357
|
+
haystack hooks install-session # Auto-detect CLIs
|
|
358
|
+
haystack hooks install-session --cli claude # Claude Code only
|
|
359
|
+
haystack hooks install-session --cli all # All detected CLIs
|
|
360
|
+
`)
|
|
361
|
+
.action(installSessionHooks);
|
|
362
|
+
hooks
|
|
363
|
+
.command('session-status')
|
|
364
|
+
.description('Check session hook installation status')
|
|
365
|
+
.action(sessionHooksStatus);
|
|
366
|
+
// Policy subcommands
|
|
367
|
+
const policy = program
|
|
368
|
+
.command('policy')
|
|
369
|
+
.description('Manage review policies (.haystack/review-policy.md)');
|
|
370
|
+
policy
|
|
371
|
+
.command('list')
|
|
372
|
+
.description('List all review policies')
|
|
373
|
+
.action(listPolicies);
|
|
374
|
+
policy
|
|
375
|
+
.command('add [name]')
|
|
376
|
+
.description('Add a new review policy interactively')
|
|
377
|
+
.addHelpText('after', `
|
|
378
|
+
This command prompts for:
|
|
379
|
+
• Policy name (e.g., "Infrastructure changes")
|
|
380
|
+
• File patterns (comma-separated globs, e.g., "terraform/**,*.tf")
|
|
381
|
+
• Severity (critical, high, medium, low)
|
|
382
|
+
• Reason (why this requires human review)
|
|
383
|
+
|
|
384
|
+
Examples:
|
|
385
|
+
haystack policy add # Interactive add
|
|
386
|
+
haystack policy add "Database changes" # Start with name
|
|
387
|
+
`)
|
|
388
|
+
.action(addPolicy);
|
|
389
|
+
policy
|
|
390
|
+
.command('remove <name>')
|
|
391
|
+
.description('Remove a review policy by name')
|
|
392
|
+
.action(removePolicy);
|
|
393
|
+
policy
|
|
394
|
+
.command('add-instruction [text]')
|
|
395
|
+
.description('Add a semantic review instruction')
|
|
396
|
+
.addHelpText('after', `
|
|
397
|
+
Instructions are natural language directives that override default review behavior.
|
|
398
|
+
They apply to all PRs (not file-pattern-gated).
|
|
399
|
+
|
|
400
|
+
Examples:
|
|
401
|
+
haystack policy add-instruction "Never flag weak test coverage as needing review"
|
|
402
|
+
haystack policy add-instruction # Interactive prompt
|
|
403
|
+
`)
|
|
404
|
+
.action(addInstruction);
|
|
405
|
+
policy
|
|
406
|
+
.command('init')
|
|
407
|
+
.description('Create initial review-policy.md with example policies')
|
|
408
|
+
.option('-f, --force', 'Overwrite existing file')
|
|
409
|
+
.addHelpText('after', `
|
|
410
|
+
Creates .haystack/review-policy.md with sensible defaults:
|
|
411
|
+
• Infrastructure changes (terraform, pulumi, cdk)
|
|
412
|
+
• Secret files (*.secret*, *.env*, credentials)
|
|
413
|
+
• CI/CD pipelines (GitHub Actions, GitLab CI, etc.)
|
|
414
|
+
|
|
415
|
+
The generated policies appear in Haystack's "Human Review Needed"
|
|
416
|
+
section when a PR touches matching files.
|
|
417
|
+
`)
|
|
418
|
+
.action(initPolicies);
|
|
189
419
|
// Show help if no command provided
|
|
190
420
|
if (process.argv.length === 2) {
|
|
191
421
|
program.help();
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detection utilities for framework, monorepo, and service configuration.
|
|
3
|
+
* Used by the setup wizard to provide smart defaults.
|
|
4
|
+
*/
|
|
5
|
+
export interface DetectedFramework {
|
|
6
|
+
name: string;
|
|
7
|
+
devCommand: string;
|
|
8
|
+
port: number;
|
|
9
|
+
readyPattern: string;
|
|
10
|
+
configFile: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DetectedService {
|
|
13
|
+
name: string;
|
|
14
|
+
root: string;
|
|
15
|
+
type: 'server' | 'batch';
|
|
16
|
+
framework?: DetectedFramework;
|
|
17
|
+
/** Services this service depends on (detected from proxy config) */
|
|
18
|
+
depends_on?: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface ProxyTarget {
|
|
21
|
+
/** Path pattern being proxied (e.g., "/api") */
|
|
22
|
+
path: string;
|
|
23
|
+
/** Target URL (e.g., "http://localhost:8787") */
|
|
24
|
+
target: string;
|
|
25
|
+
/** Port extracted from target */
|
|
26
|
+
port: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ProjectDetection {
|
|
29
|
+
isMonorepo: boolean;
|
|
30
|
+
packageManager: 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
31
|
+
frameworks: DetectedFramework[];
|
|
32
|
+
services: DetectedService[];
|
|
33
|
+
/** Proxy targets detected from config files */
|
|
34
|
+
proxyTargets: ProxyTarget[];
|
|
35
|
+
suggestions: {
|
|
36
|
+
projectName: string;
|
|
37
|
+
devCommand: string;
|
|
38
|
+
port: number;
|
|
39
|
+
readyPattern: string;
|
|
40
|
+
authBypassEnv?: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Main detection function - analyzes a project directory
|
|
45
|
+
*/
|
|
46
|
+
export declare function detectProject(rootDir?: string): ProjectDetection;
|
|
47
|
+
/**
|
|
48
|
+
* Format detection results for display to user
|
|
49
|
+
*/
|
|
50
|
+
export declare function formatDetectionSummary(detection: ProjectDetection): string;
|