@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/triage.js
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* triage command —
|
|
2
|
+
* triage command — Show Haystack analysis results for a PR.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Designed for coding agents to consume. Shows two things:
|
|
5
|
+
* 1. What the auto-fixer is already handling (so the agent doesn't duplicate work)
|
|
6
|
+
* 2. Findings with attribution and fix prompts (so the agent knows what to fix)
|
|
7
|
+
*
|
|
8
|
+
* When called without a PR identifier, reads the last submitted PR from
|
|
9
|
+
* `.haystack/pending-submit.json` (written by `haystack submit`).
|
|
6
10
|
*
|
|
7
11
|
* Accepts a PR identifier in several formats:
|
|
12
|
+
* haystack triage # Last submitted PR
|
|
8
13
|
* haystack triage 123 # Uses current repo's owner/repo
|
|
9
14
|
* haystack triage owner/repo#123 # Fully qualified
|
|
10
15
|
* haystack triage https://github.com/owner/repo/pull/123
|
|
16
|
+
*
|
|
17
|
+
* Output modes:
|
|
18
|
+
* --hook Minimal single-line for session-start hooks
|
|
19
|
+
* --json Machine-readable JSON
|
|
20
|
+
* (default) Rich terminal output
|
|
11
21
|
*/
|
|
12
22
|
import chalk from 'chalk';
|
|
13
|
-
import { checkAnalysisReady, fetchRatingSynthesis } from '../utils/analysis-api.js';
|
|
23
|
+
import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
|
|
14
24
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
15
25
|
import { loadToken } from './login.js';
|
|
26
|
+
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
16
27
|
// ============================================================================
|
|
17
28
|
// PR identifier parsing
|
|
18
29
|
// ============================================================================
|
|
@@ -57,169 +68,120 @@ function parsePRIdentifier(identifier) {
|
|
|
57
68
|
// ============================================================================
|
|
58
69
|
// Output formatting
|
|
59
70
|
// ============================================================================
|
|
60
|
-
function
|
|
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) {
|
|
71
|
+
function printRichOutput(pr, synthesis, manifest) {
|
|
87
72
|
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
88
|
-
console.log(chalk.bold(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
}
|
|
73
|
+
console.log(`\n ${chalk.bold(`Haystack Triage: ${pr.owner}/${pr.repo}#${pr.prNumber}`)}\n`);
|
|
74
|
+
// Auto-fixer status — what's being fixed vs what it skipped
|
|
75
|
+
if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
|
|
76
|
+
if (manifest.fixedItems.length > 0) {
|
|
77
|
+
console.log(chalk.bold(' Auto-fixer is handling:\n'));
|
|
78
|
+
for (const item of manifest.fixedItems) {
|
|
79
|
+
console.log(` - [${item.category}] ${item.summary}`);
|
|
134
80
|
}
|
|
135
81
|
console.log('');
|
|
136
82
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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}`);
|
|
83
|
+
if (manifest.leftForReview.length > 0) {
|
|
84
|
+
console.log(chalk.bold(' Auto-fixer skipped:\n'));
|
|
85
|
+
for (const item of manifest.leftForReview) {
|
|
86
|
+
console.log(` - [${item.category}] ${item.summary}`);
|
|
87
|
+
console.log(` ${chalk.dim(item.reason)}`);
|
|
88
|
+
}
|
|
147
89
|
console.log('');
|
|
148
90
|
}
|
|
149
91
|
}
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
console.log(` ${
|
|
158
|
-
if (
|
|
159
|
-
|
|
92
|
+
// Findings — the actionable output for agents
|
|
93
|
+
const findings = synthesis.synthesisDisplay || [];
|
|
94
|
+
if (findings.length > 0) {
|
|
95
|
+
console.log(chalk.bold(' Findings:\n'));
|
|
96
|
+
for (let i = 0; i < findings.length; i++) {
|
|
97
|
+
const finding = findings[i];
|
|
98
|
+
const sourceTag = finding.source ? ` (${finding.source})` : '';
|
|
99
|
+
console.log(` ${i + 1}. [${finding.category}]${sourceTag} ${finding.summary}`);
|
|
100
|
+
if (finding.detail) {
|
|
101
|
+
for (const line of finding.detail.split('\n')) {
|
|
102
|
+
console.log(` ${line}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (finding.agentFixPrompt) {
|
|
106
|
+
const fixLines = finding.agentFixPrompt.split('\n');
|
|
107
|
+
console.log(` ${chalk.dim('Fix:')} ${fixLines[0]}`);
|
|
108
|
+
for (const line of fixLines.slice(1)) {
|
|
109
|
+
console.log(` ${line}`);
|
|
110
|
+
}
|
|
160
111
|
}
|
|
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
112
|
console.log('');
|
|
174
113
|
}
|
|
175
114
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
|
|
179
|
-
console.log('');
|
|
115
|
+
else if (!manifest || manifest.fixedItems.length === 0) {
|
|
116
|
+
console.log(chalk.dim(' No findings.\n'));
|
|
180
117
|
}
|
|
181
|
-
console.log(` ${chalk.dim(
|
|
182
|
-
console.log('');
|
|
118
|
+
console.log(` ${chalk.dim(reviewUrl)}\n`);
|
|
183
119
|
}
|
|
184
|
-
function printJsonOutput(pr, synthesis) {
|
|
185
|
-
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
120
|
+
function printJsonOutput(pr, synthesis, manifest) {
|
|
186
121
|
const output = {
|
|
187
122
|
owner: pr.owner,
|
|
188
123
|
repo: pr.repo,
|
|
189
124
|
prNumber: pr.prNumber,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
needsHumanReview: synthesis.needsHumanReview,
|
|
195
|
-
findings: (synthesis.synthesisDisplay || []).map(f => ({
|
|
125
|
+
rating: synthesis.haystackRating,
|
|
126
|
+
autoFixerHandling: manifest?.fixedItems ?? [],
|
|
127
|
+
autoFixerSkipped: manifest?.leftForReview ?? [],
|
|
128
|
+
findings: (synthesis.synthesisDisplay || []).map((f) => ({
|
|
196
129
|
category: f.category,
|
|
197
130
|
summary: f.summary,
|
|
198
131
|
detail: f.detail,
|
|
199
132
|
agentFixPrompt: f.agentFixPrompt || null,
|
|
200
133
|
source: f.source || null,
|
|
201
134
|
})),
|
|
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
135
|
};
|
|
214
136
|
console.log(JSON.stringify(output, null, 2));
|
|
215
137
|
}
|
|
138
|
+
function ratingStars(rating) {
|
|
139
|
+
const filled = '\u2605'; // ★
|
|
140
|
+
const empty = '\u2606'; // ☆
|
|
141
|
+
return filled.repeat(rating) + empty.repeat(5 - rating);
|
|
142
|
+
}
|
|
143
|
+
function printHookOutput(pr, synthesis, manifest, status) {
|
|
144
|
+
const label = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
|
|
145
|
+
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
146
|
+
if (status === 'pending') {
|
|
147
|
+
console.log(`[Haystack] ${label}: Analysis in progress...`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (status === 'failed') {
|
|
151
|
+
console.log(`[Haystack] ${label}: Analysis failed`);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (status === 'stale') {
|
|
155
|
+
console.log(`[Haystack] ${label}: Stale (submitted > 2h ago)`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!synthesis) {
|
|
159
|
+
console.log(`[Haystack] ${label}: Analysis in progress...`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const findings = synthesis.synthesisDisplay || [];
|
|
163
|
+
const stars = ratingStars(synthesis.haystackRating);
|
|
164
|
+
const fixingCount = manifest?.fixedItems?.length ?? 0;
|
|
165
|
+
if (findings.length === 0 && fixingCount === 0) {
|
|
166
|
+
console.log(`[Haystack] \u2705 ${label} ${stars}: Good to merge`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const parts = [];
|
|
170
|
+
if (findings.length > 0)
|
|
171
|
+
parts.push(`${findings.length} finding(s)`);
|
|
172
|
+
if (fixingCount > 0)
|
|
173
|
+
parts.push(`${fixingCount} being auto-fixed`);
|
|
174
|
+
console.log(`[Haystack] \u26A0\uFE0F ${label} ${stars}: Needs your input (${parts.join(', ')})`);
|
|
175
|
+
console.log(` ${reviewUrl}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
216
178
|
// ============================================================================
|
|
217
179
|
// Polling
|
|
218
180
|
// ============================================================================
|
|
219
181
|
const POLL_INTERVAL_MS = 5_000;
|
|
220
182
|
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
221
|
-
async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
|
|
222
|
-
const deadline = Date.now() +
|
|
183
|
+
async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token, quiet) {
|
|
184
|
+
const deadline = Date.now() + timeoutMs;
|
|
223
185
|
let consecutiveErrors = 0;
|
|
224
186
|
while (Date.now() < deadline) {
|
|
225
187
|
const result = await checkAnalysisReady(owner, repo, prNumber, token);
|
|
@@ -241,21 +203,55 @@ async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
|
|
|
241
203
|
return 'pending';
|
|
242
204
|
}
|
|
243
205
|
// ============================================================================
|
|
206
|
+
// PR resolution
|
|
207
|
+
// ============================================================================
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a PR from an explicit identifier or from pending-submit state.
|
|
210
|
+
* Returns null (and handles output) if no PR can be resolved.
|
|
211
|
+
*/
|
|
212
|
+
function resolvePR(identifier, options) {
|
|
213
|
+
if (identifier) {
|
|
214
|
+
try {
|
|
215
|
+
return parsePRIdentifier(identifier);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// No arg — read from pending state
|
|
223
|
+
const pending = loadPendingSubmit();
|
|
224
|
+
if (!pending) {
|
|
225
|
+
if (options.json) {
|
|
226
|
+
console.log(JSON.stringify({ status: 'none' }));
|
|
227
|
+
}
|
|
228
|
+
else if (!options.hook) {
|
|
229
|
+
console.log(chalk.dim('No pending submits. Pass a PR number to triage a specific PR.\n'));
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return { owner: pending.owner, repo: pending.repo, prNumber: pending.prNumber };
|
|
234
|
+
}
|
|
235
|
+
// ============================================================================
|
|
244
236
|
// Main command
|
|
245
237
|
// ============================================================================
|
|
246
238
|
export async function triageCommand(identifier, options) {
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
process.exit(1);
|
|
239
|
+
// Handle --clear
|
|
240
|
+
if (options.clear) {
|
|
241
|
+
clearPendingSubmit();
|
|
242
|
+
if (!options.hook) {
|
|
243
|
+
console.log(chalk.dim('Pending submit state cleared.'));
|
|
244
|
+
}
|
|
245
|
+
return;
|
|
255
246
|
}
|
|
247
|
+
// Resolve PR identifier — explicit arg or fall back to pending-submit.json
|
|
248
|
+
const pr = resolvePR(identifier, options);
|
|
249
|
+
if (!pr)
|
|
250
|
+
return;
|
|
256
251
|
// Load GitHub token for private repo auth
|
|
257
252
|
const token = await loadToken() || undefined;
|
|
258
|
-
|
|
253
|
+
const isQuiet = options.hook || options.json;
|
|
254
|
+
if (!isQuiet) {
|
|
259
255
|
console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
|
|
260
256
|
}
|
|
261
257
|
// Check if analysis is ready — resolve to a terminal state before fetching
|
|
@@ -265,56 +261,74 @@ export async function triageCommand(identifier, options) {
|
|
|
265
261
|
analysisReady = true;
|
|
266
262
|
}
|
|
267
263
|
else if (readyResult.status === 'pending') {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
264
|
+
// Hook mode: short 5s poll. --no-wait: skip. Default: full poll.
|
|
265
|
+
if (options.hook) {
|
|
266
|
+
const hookResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, 5_000, token, true);
|
|
267
|
+
if (hookResult === 'ready') {
|
|
268
|
+
analysisReady = true;
|
|
273
269
|
}
|
|
274
270
|
else {
|
|
275
|
-
|
|
276
|
-
|
|
271
|
+
updatePendingSubmitIfExists(pr);
|
|
272
|
+
printHookOutput(pr, null, null, 'pending');
|
|
273
|
+
return;
|
|
277
274
|
}
|
|
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
275
|
}
|
|
291
|
-
else if (
|
|
276
|
+
else if (options.wait === false) {
|
|
292
277
|
if (options.json) {
|
|
293
278
|
console.log(JSON.stringify({
|
|
294
279
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
295
280
|
}, null, 2));
|
|
296
281
|
}
|
|
297
282
|
else {
|
|
298
|
-
console.log(chalk.yellow('\n
|
|
299
|
-
console.log(chalk.dim('
|
|
283
|
+
console.log(chalk.yellow('\n Analysis is still in progress.'));
|
|
284
|
+
console.log(chalk.dim(' Re-run without --no-wait to poll until complete.\n'));
|
|
300
285
|
}
|
|
301
286
|
return;
|
|
302
287
|
}
|
|
303
288
|
else {
|
|
304
|
-
//
|
|
305
|
-
if (options.json) {
|
|
306
|
-
console.log(
|
|
307
|
-
|
|
308
|
-
|
|
289
|
+
// Full poll
|
|
290
|
+
if (!options.json) {
|
|
291
|
+
console.log(chalk.dim(' Analysis in progress, waiting...'));
|
|
292
|
+
}
|
|
293
|
+
const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, POLL_TIMEOUT_MS, token, options.json);
|
|
294
|
+
if (!options.json) {
|
|
295
|
+
process.stderr.write('\r' + ' '.repeat(60) + '\r');
|
|
296
|
+
}
|
|
297
|
+
if (waitResult === 'ready') {
|
|
298
|
+
analysisReady = true;
|
|
299
|
+
}
|
|
300
|
+
else if (waitResult === 'pending') {
|
|
301
|
+
if (options.json) {
|
|
302
|
+
console.log(JSON.stringify({
|
|
303
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
304
|
+
}, null, 2));
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.log(chalk.yellow('\n Timed out waiting for analysis.'));
|
|
308
|
+
console.log(chalk.dim(' Try again later: ') + chalk.cyan(`haystack triage ${pr.owner}/${pr.repo}#${pr.prNumber}\n`));
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
309
311
|
}
|
|
310
312
|
else {
|
|
311
|
-
|
|
313
|
+
// waitResult === 'error'
|
|
314
|
+
if (options.json) {
|
|
315
|
+
console.log(JSON.stringify({
|
|
316
|
+
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
|
|
317
|
+
}, null, 2));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.error(chalk.red('\n Analysis check failed.\n'));
|
|
321
|
+
}
|
|
322
|
+
process.exit(1);
|
|
312
323
|
}
|
|
313
|
-
process.exit(1);
|
|
314
324
|
}
|
|
315
325
|
}
|
|
316
326
|
else {
|
|
317
327
|
// readyResult.status === 'error'
|
|
328
|
+
if (options.hook) {
|
|
329
|
+
printHookOutput(pr, null, null, 'failed');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
318
332
|
if (options.json) {
|
|
319
333
|
console.log(JSON.stringify({
|
|
320
334
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
@@ -328,9 +342,18 @@ export async function triageCommand(identifier, options) {
|
|
|
328
342
|
}
|
|
329
343
|
if (!analysisReady)
|
|
330
344
|
return;
|
|
331
|
-
// Fetch the full rating synthesis
|
|
332
|
-
const synthesis = await
|
|
345
|
+
// Fetch the full rating synthesis and auto-fix manifest in parallel
|
|
346
|
+
const [synthesis, autoFixManifest] = await Promise.all([
|
|
347
|
+
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
348
|
+
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
349
|
+
]);
|
|
350
|
+
// Update pending state if this PR matches the pending submit
|
|
351
|
+
updatePendingSubmitIfExists(pr, synthesis ? 'completed' : undefined);
|
|
333
352
|
if (!synthesis) {
|
|
353
|
+
if (options.hook) {
|
|
354
|
+
printHookOutput(pr, null, null, 'pending');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
334
357
|
if (options.json) {
|
|
335
358
|
console.log(JSON.stringify({
|
|
336
359
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
@@ -345,10 +368,86 @@ export async function triageCommand(identifier, options) {
|
|
|
345
368
|
process.exit(1);
|
|
346
369
|
}
|
|
347
370
|
// Output
|
|
348
|
-
if (options.
|
|
349
|
-
|
|
371
|
+
if (options.hook) {
|
|
372
|
+
printHookOutput(pr, synthesis, autoFixManifest);
|
|
373
|
+
}
|
|
374
|
+
else if (options.json) {
|
|
375
|
+
printJsonOutput(pr, synthesis, autoFixManifest);
|
|
350
376
|
}
|
|
351
377
|
else {
|
|
352
|
-
printRichOutput(pr, synthesis);
|
|
378
|
+
printRichOutput(pr, synthesis, autoFixManifest);
|
|
379
|
+
}
|
|
380
|
+
// Record findings-seen marker (fire-and-forget — failure is non-fatal)
|
|
381
|
+
if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0 && token) {
|
|
382
|
+
const findingKeys = synthesis.synthesisDisplay.map((f) => `${f.category}|${f.summary}`);
|
|
383
|
+
recordFindingsSeen(pr, token, findingKeys).catch((err) => {
|
|
384
|
+
// Non-blocking but logged — don't swallow silently
|
|
385
|
+
if (process.env.DEBUG) {
|
|
386
|
+
console.error(chalk.dim(` [debug] findings-seen write failed: ${err}`));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Update the pending-submit state if the given PR matches the pending one.
|
|
393
|
+
* Best-effort — failure is silently ignored.
|
|
394
|
+
*/
|
|
395
|
+
function updatePendingSubmitIfExists(pr, status) {
|
|
396
|
+
try {
|
|
397
|
+
const pending = loadPendingSubmit();
|
|
398
|
+
if (!pending)
|
|
399
|
+
return;
|
|
400
|
+
if (pending.owner !== pr.owner || pending.repo !== pr.repo || pending.prNumber !== pr.prNumber)
|
|
401
|
+
return;
|
|
402
|
+
if (pending.status !== 'polling')
|
|
403
|
+
return;
|
|
404
|
+
if (status === 'completed') {
|
|
405
|
+
updatePendingSubmit({
|
|
406
|
+
status: 'completed',
|
|
407
|
+
resolvedAt: new Date().toISOString(),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Non-fatal
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Record that the user has seen analysis findings for this PR.
|
|
417
|
+
* Fire-and-forget: failure is silently ignored.
|
|
418
|
+
*/
|
|
419
|
+
async function recordFindingsSeen(pr, token, findingKeys) {
|
|
420
|
+
// Get PR head SHA
|
|
421
|
+
const ghResp = await fetch(`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, {
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: `Bearer ${token}`,
|
|
424
|
+
Accept: 'application/vnd.github.v3+json',
|
|
425
|
+
'User-Agent': 'Haystack-CLI',
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
if (!ghResp.ok) {
|
|
429
|
+
throw new Error(`GitHub API returned ${ghResp.status}`);
|
|
430
|
+
}
|
|
431
|
+
const ghData = await ghResp.json();
|
|
432
|
+
const sha = ghData?.head?.sha;
|
|
433
|
+
if (typeof sha !== 'string' || !sha) {
|
|
434
|
+
throw new Error('Unexpected GitHub response: missing head.sha');
|
|
435
|
+
}
|
|
436
|
+
const resp = await fetch('https://haystackeditor.com/api/findings-seen', {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers: {
|
|
439
|
+
Authorization: `Bearer ${token}`,
|
|
440
|
+
'Content-Type': 'application/json',
|
|
441
|
+
'User-Agent': 'Haystack-CLI',
|
|
442
|
+
},
|
|
443
|
+
body: JSON.stringify({
|
|
444
|
+
repoFullName: `${pr.owner}/${pr.repo}`,
|
|
445
|
+
prNumber: pr.prNumber,
|
|
446
|
+
commitSha: sha,
|
|
447
|
+
findingKeys,
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
if (!resp.ok) {
|
|
451
|
+
throw new Error(`findings-seen API returned ${resp.status}`);
|
|
353
452
|
}
|
|
354
453
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
* npx @haystackeditor/cli status # Check configuration
|
|
12
12
|
* npx @haystackeditor/cli login # Authenticate with GitHub
|
|
13
13
|
* npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
|
|
14
|
-
* npx @haystackeditor/cli
|
|
14
|
+
* npx @haystackeditor/cli triage # Check last submitted PR
|
|
15
15
|
* npx @haystackeditor/cli triage 123 # View analysis results for a PR
|
|
16
16
|
* npx @haystackeditor/cli dismiss 123 # Dismiss findings for a PR
|
|
17
17
|
* npx @haystackeditor/cli mark-reviewed 123 # Mark review as not needed
|
|
18
|
+
* npx @haystackeditor/cli request-review 123 # Tag a PR as needing human review
|
|
18
19
|
* npx @haystackeditor/cli pr-status 123 # Show PR status in Haystack pipeline
|
|
19
20
|
* npx @haystackeditor/cli config # Manage preferences
|
|
20
21
|
*/
|