@haystackeditor/cli 0.11.0 → 0.12.1
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 +9 -12
- 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 +15 -5
- 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/setup.js +21 -10
- package/dist/commands/submit.d.ts +1 -0
- package/dist/commands/submit.js +11 -18
- package/dist/commands/triage.d.ts +16 -4
- package/dist/commands/triage.js +210 -203
- package/dist/index.d.ts +1 -1
- package/dist/index.js +31 -33
- package/dist/triage/traces.js +9 -3
- package/dist/types.d.ts +14 -0
- package/dist/types.js +2 -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
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,209 +68,120 @@ function parsePRIdentifier(identifier) {
|
|
|
57
68
|
// ============================================================================
|
|
58
69
|
// Output formatting
|
|
59
70
|
// ============================================================================
|
|
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
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
|
-
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
|
-
// Auto-fix manifest — show what the fixer did vs left for human review
|
|
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
|
|
121
75
|
if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
|
|
122
|
-
console.log(chalk.bold('\n Auto-Fixer Results\n'));
|
|
123
76
|
if (manifest.fixedItems.length > 0) {
|
|
124
|
-
console.log(
|
|
77
|
+
console.log(chalk.bold(' Auto-fixer is handling:\n'));
|
|
125
78
|
for (const item of manifest.fixedItems) {
|
|
126
|
-
console.log(`
|
|
79
|
+
console.log(` - [${item.category}] ${item.summary}`);
|
|
127
80
|
}
|
|
128
81
|
console.log('');
|
|
129
82
|
}
|
|
130
83
|
if (manifest.leftForReview.length > 0) {
|
|
131
|
-
console.log(
|
|
84
|
+
console.log(chalk.bold(' Auto-fixer skipped:\n'));
|
|
132
85
|
for (const item of manifest.leftForReview) {
|
|
133
|
-
console.log(`
|
|
134
|
-
console.log(`
|
|
86
|
+
console.log(` - [${item.category}] ${item.summary}`);
|
|
87
|
+
console.log(` ${chalk.dim(item.reason)}`);
|
|
135
88
|
}
|
|
136
89
|
console.log('');
|
|
137
90
|
}
|
|
138
91
|
}
|
|
139
|
-
// Findings
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
console.log(` ${finding.summary}`);
|
|
151
|
-
if (finding.detail) {
|
|
152
|
-
const detailLines = finding.detail.split('\n');
|
|
153
|
-
for (const line of detailLines) {
|
|
154
|
-
console.log(` ${chalk.dim(line)}`);
|
|
155
|
-
}
|
|
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}`);
|
|
156
103
|
}
|
|
157
|
-
console.log('');
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// Previously reported findings — dimmed
|
|
161
|
-
if (knownFindings.length > 0) {
|
|
162
|
-
console.log(chalk.dim(` Previously Reported (${knownFindings.length})\n`));
|
|
163
|
-
for (let i = 0; i < knownFindings.length; i++) {
|
|
164
|
-
const finding = knownFindings[i];
|
|
165
|
-
const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
|
|
166
|
-
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.dim(finding.category)}${sourceTag}`);
|
|
167
|
-
console.log(` ${chalk.dim(finding.summary)}`);
|
|
168
|
-
console.log('');
|
|
169
104
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
for (const bug of confirmedBugs) {
|
|
177
|
-
const sourceTag = bug.source ? chalk.dim(` (via ${bug.source.type}${bug.source.author ? `: ${bug.source.author}` : ''})`) : '';
|
|
178
|
-
console.log(` ${severityIcon(bug.assessedSeverity)} ${chalk.bold(bug.originalTitle)}${sourceTag}`);
|
|
179
|
-
console.log(` ${chalk.dim('Severity:')} ${bug.assessedSeverity}${bug.likelihood ? `, ${bug.likelihood} occurrence` : ''}`);
|
|
180
|
-
console.log(` ${bug.rationale}`);
|
|
181
|
-
console.log('');
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// Human review reasons
|
|
185
|
-
if (synthesis.humanReviewReasons && synthesis.humanReviewReasons.length > 0) {
|
|
186
|
-
console.log(chalk.bold(' Human Review Required\n'));
|
|
187
|
-
for (const reason of synthesis.humanReviewReasons) {
|
|
188
|
-
const icon = reason.severity === 'high' || reason.severity === 'critical'
|
|
189
|
-
? chalk.red('!')
|
|
190
|
-
: chalk.yellow('·');
|
|
191
|
-
console.log(` ${icon} ${reason.reason}`);
|
|
192
|
-
if (reason.source) {
|
|
193
|
-
console.log(` ${chalk.dim(`Source: ${reason.source}`)}`);
|
|
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
|
+
}
|
|
194
111
|
}
|
|
195
|
-
}
|
|
196
|
-
console.log('');
|
|
197
|
-
}
|
|
198
|
-
// Agent fix prompts — only for new findings (known issues were already triaged)
|
|
199
|
-
const fixableFindings = (synthesis.synthesisDisplay || []).filter((f) => f.agentFixPrompt && f.isNew !== false);
|
|
200
|
-
if (fixableFindings.length > 0) {
|
|
201
|
-
console.log(chalk.bold(' Agent Fix Prompts'));
|
|
202
|
-
console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
|
|
203
|
-
for (let i = 0; i < fixableFindings.length; i++) {
|
|
204
|
-
const finding = fixableFindings[i];
|
|
205
|
-
console.log(chalk.cyan(` --- Fix ${i + 1}: ${finding.category} ---`));
|
|
206
|
-
console.log(` ${finding.agentFixPrompt}`);
|
|
207
112
|
console.log('');
|
|
208
113
|
}
|
|
209
114
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
|
|
213
|
-
console.log('');
|
|
115
|
+
else if (!manifest || manifest.fixedItems.length === 0) {
|
|
116
|
+
console.log(chalk.dim(' No findings.\n'));
|
|
214
117
|
}
|
|
215
|
-
console.log(` ${chalk.dim(
|
|
216
|
-
console.log('');
|
|
118
|
+
console.log(` ${chalk.dim(reviewUrl)}\n`);
|
|
217
119
|
}
|
|
218
120
|
function printJsonOutput(pr, synthesis, manifest) {
|
|
219
|
-
const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
|
|
220
121
|
const output = {
|
|
221
122
|
owner: pr.owner,
|
|
222
123
|
repo: pr.repo,
|
|
223
124
|
prNumber: pr.prNumber,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
|
|
228
|
-
needsHumanReview: synthesis.needsHumanReview,
|
|
125
|
+
rating: synthesis.haystackRating,
|
|
126
|
+
autoFixerHandling: manifest?.fixedItems ?? [],
|
|
127
|
+
autoFixerSkipped: manifest?.leftForReview ?? [],
|
|
229
128
|
findings: (synthesis.synthesisDisplay || []).map((f) => ({
|
|
230
129
|
category: f.category,
|
|
231
130
|
summary: f.summary,
|
|
232
131
|
detail: f.detail,
|
|
233
132
|
agentFixPrompt: f.agentFixPrompt || null,
|
|
234
133
|
source: f.source || null,
|
|
235
|
-
isNew: f.isNew ?? null,
|
|
236
134
|
})),
|
|
237
|
-
verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
|
|
238
|
-
title: b.originalTitle,
|
|
239
|
-
severity: b.assessedSeverity,
|
|
240
|
-
likelihood: b.likelihood,
|
|
241
|
-
rationale: b.rationale,
|
|
242
|
-
source: b.source || null,
|
|
243
|
-
})),
|
|
244
|
-
humanReviewReasons: synthesis.humanReviewReasons || [],
|
|
245
|
-
verification: synthesis.verification || null,
|
|
246
|
-
verificationConfidence: synthesis.verificationConfidence || null,
|
|
247
|
-
autoFixManifest: manifest ? {
|
|
248
|
-
fixedItems: manifest.fixedItems,
|
|
249
|
-
leftForReview: manifest.leftForReview,
|
|
250
|
-
timestamp: manifest.timestamp,
|
|
251
|
-
} : null,
|
|
252
|
-
reviewUrl,
|
|
253
135
|
};
|
|
254
136
|
console.log(JSON.stringify(output, null, 2));
|
|
255
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
|
+
}
|
|
256
178
|
// ============================================================================
|
|
257
179
|
// Polling
|
|
258
180
|
// ============================================================================
|
|
259
181
|
const POLL_INTERVAL_MS = 5_000;
|
|
260
182
|
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
261
|
-
async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
|
|
262
|
-
const deadline = Date.now() +
|
|
183
|
+
async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token, quiet) {
|
|
184
|
+
const deadline = Date.now() + timeoutMs;
|
|
263
185
|
let consecutiveErrors = 0;
|
|
264
186
|
while (Date.now() < deadline) {
|
|
265
187
|
const result = await checkAnalysisReady(owner, repo, prNumber, token);
|
|
@@ -281,21 +203,55 @@ async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
|
|
|
281
203
|
return 'pending';
|
|
282
204
|
}
|
|
283
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
|
+
// ============================================================================
|
|
284
236
|
// Main command
|
|
285
237
|
// ============================================================================
|
|
286
238
|
export async function triageCommand(identifier, options) {
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
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;
|
|
295
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;
|
|
296
251
|
// Load GitHub token for private repo auth
|
|
297
252
|
const token = await loadToken() || undefined;
|
|
298
|
-
|
|
253
|
+
const isQuiet = options.hook || options.json;
|
|
254
|
+
if (!isQuiet) {
|
|
299
255
|
console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
|
|
300
256
|
}
|
|
301
257
|
// Check if analysis is ready — resolve to a terminal state before fetching
|
|
@@ -305,56 +261,74 @@ export async function triageCommand(identifier, options) {
|
|
|
305
261
|
analysisReady = true;
|
|
306
262
|
}
|
|
307
263
|
else if (readyResult.status === 'pending') {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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;
|
|
313
269
|
}
|
|
314
270
|
else {
|
|
315
|
-
|
|
316
|
-
|
|
271
|
+
updatePendingSubmitIfExists(pr);
|
|
272
|
+
printHookOutput(pr, null, null, 'pending');
|
|
273
|
+
return;
|
|
317
274
|
}
|
|
318
|
-
return;
|
|
319
275
|
}
|
|
320
|
-
|
|
321
|
-
if (!options.json) {
|
|
322
|
-
console.log(chalk.dim(' Analysis in progress, waiting...'));
|
|
323
|
-
}
|
|
324
|
-
const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, token, options.json);
|
|
325
|
-
if (!options.json) {
|
|
326
|
-
process.stderr.write('\r' + ' '.repeat(60) + '\r');
|
|
327
|
-
}
|
|
328
|
-
if (waitResult === 'ready') {
|
|
329
|
-
analysisReady = true;
|
|
330
|
-
}
|
|
331
|
-
else if (waitResult === 'pending') {
|
|
276
|
+
else if (options.wait === false) {
|
|
332
277
|
if (options.json) {
|
|
333
278
|
console.log(JSON.stringify({
|
|
334
279
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
|
|
335
280
|
}, null, 2));
|
|
336
281
|
}
|
|
337
282
|
else {
|
|
338
|
-
console.log(chalk.yellow('\n
|
|
339
|
-
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'));
|
|
340
285
|
}
|
|
341
286
|
return;
|
|
342
287
|
}
|
|
343
288
|
else {
|
|
344
|
-
//
|
|
345
|
-
if (options.json) {
|
|
346
|
-
console.log(
|
|
347
|
-
|
|
348
|
-
|
|
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;
|
|
349
311
|
}
|
|
350
312
|
else {
|
|
351
|
-
|
|
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);
|
|
352
323
|
}
|
|
353
|
-
process.exit(1);
|
|
354
324
|
}
|
|
355
325
|
}
|
|
356
326
|
else {
|
|
357
327
|
// readyResult.status === 'error'
|
|
328
|
+
if (options.hook) {
|
|
329
|
+
printHookOutput(pr, null, null, 'failed');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
358
332
|
if (options.json) {
|
|
359
333
|
console.log(JSON.stringify({
|
|
360
334
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
@@ -373,7 +347,13 @@ export async function triageCommand(identifier, options) {
|
|
|
373
347
|
fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
|
|
374
348
|
fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
|
|
375
349
|
]);
|
|
350
|
+
// Update pending state if this PR matches the pending submit
|
|
351
|
+
updatePendingSubmitIfExists(pr, synthesis ? 'completed' : undefined);
|
|
376
352
|
if (!synthesis) {
|
|
353
|
+
if (options.hook) {
|
|
354
|
+
printHookOutput(pr, null, null, 'pending');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
377
357
|
if (options.json) {
|
|
378
358
|
console.log(JSON.stringify({
|
|
379
359
|
owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
|
|
@@ -388,7 +368,10 @@ export async function triageCommand(identifier, options) {
|
|
|
388
368
|
process.exit(1);
|
|
389
369
|
}
|
|
390
370
|
// Output
|
|
391
|
-
if (options.
|
|
371
|
+
if (options.hook) {
|
|
372
|
+
printHookOutput(pr, synthesis, autoFixManifest);
|
|
373
|
+
}
|
|
374
|
+
else if (options.json) {
|
|
392
375
|
printJsonOutput(pr, synthesis, autoFixManifest);
|
|
393
376
|
}
|
|
394
377
|
else {
|
|
@@ -405,6 +388,30 @@ export async function triageCommand(identifier, options) {
|
|
|
405
388
|
});
|
|
406
389
|
}
|
|
407
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
|
+
}
|
|
408
415
|
/**
|
|
409
416
|
* Record that the user has seen analysis findings for this PR.
|
|
410
417
|
* Fire-and-forget: failure is silently ignored.
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
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
|