@haystackeditor/cli 0.8.0 → 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 +105 -12
- package/dist/assets/hooks/agent-context/detect.ts +136 -0
- package/dist/assets/hooks/agent-context/format.ts +99 -0
- package/dist/assets/hooks/agent-context/index.ts +39 -0
- package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
- package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
- package/dist/assets/hooks/agent-context/types.ts +58 -0
- package/dist/assets/hooks/llm-rules-template.md +56 -0
- package/dist/assets/hooks/package.json +11 -0
- package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
- package/dist/assets/hooks/scripts/post-commit.sh +4 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
- package/dist/assets/hooks/scripts/pre-push.sh +25 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
- package/dist/assets/hooks/truncation-checker/index.ts +595 -0
- package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -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 +18 -12
- package/dist/commands/config.js +327 -52
- package/dist/commands/hooks.d.ts +17 -0
- package/dist/commands/hooks.js +269 -0
- 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 +8 -0
- package/dist/commands/skills.js +80 -0
- 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 +317 -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/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +226 -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.d.ts +1 -1
- package/dist/utils/skill.js +658 -1
- package/package.json +5 -3
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Submit command - Create a PR from current changes
|
|
3
|
+
*
|
|
4
|
+
* Runs pre-PR triage (code review, rules, intent drift) via parallel sub-agents,
|
|
5
|
+
* then pushes the current branch and creates a PR.
|
|
6
|
+
*
|
|
7
|
+
* PRs are routed based on flags:
|
|
8
|
+
* - Default: auto-merge queue (analysis runs, auto-merged if approved)
|
|
9
|
+
* - --review: human review required
|
|
10
|
+
* - --force: skip triage checks
|
|
11
|
+
*/
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, } from '../utils/git.js';
|
|
15
|
+
import { createPullRequest, findExistingPR, requireGitHubAuth, requestReviewers, } from '../utils/github-api.js';
|
|
16
|
+
import { runTriage } from '../triage/runner.js';
|
|
17
|
+
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
18
|
+
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
19
|
+
import { isAutoMergeEnabled } from './config.js';
|
|
20
|
+
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Helpers
|
|
23
|
+
// ============================================================================
|
|
24
|
+
function buildPRBody(commitMessages) {
|
|
25
|
+
const body = commitMessages || 'Submitted via `haystack submit`.';
|
|
26
|
+
return `${body}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
*Created via [Haystack CLI](https://haystackeditor.com)*`;
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Triage output
|
|
33
|
+
// ============================================================================
|
|
34
|
+
function severityIcon(severity) {
|
|
35
|
+
switch (severity) {
|
|
36
|
+
case 'error': return chalk.red('✗');
|
|
37
|
+
case 'warning': return chalk.yellow('!');
|
|
38
|
+
case 'info': return chalk.blue('i');
|
|
39
|
+
default: return chalk.dim('-');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function severityColor(severity) {
|
|
43
|
+
switch (severity) {
|
|
44
|
+
case 'error': return chalk.red;
|
|
45
|
+
case 'warning': return chalk.yellow;
|
|
46
|
+
case 'info': return chalk.blue;
|
|
47
|
+
default: return chalk.dim;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function checkerDisplayName(checker) {
|
|
51
|
+
switch (checker) {
|
|
52
|
+
case 'code-review': return 'Code Review';
|
|
53
|
+
case 'rules-validator': return 'Rules Validator';
|
|
54
|
+
case 'intent-drift': return 'Intent Drift';
|
|
55
|
+
default: return checker;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isCodexNetworkDisabledTriageError(err) {
|
|
59
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
60
|
+
return message.includes('CODEX_SANDBOX_NETWORK_DISABLED=1');
|
|
61
|
+
}
|
|
62
|
+
function printTriageSummary(result) {
|
|
63
|
+
console.log(chalk.bold('\n' + '━'.repeat(50)));
|
|
64
|
+
console.log(chalk.bold(' Pre-PR Triage Results'));
|
|
65
|
+
console.log(chalk.bold('━'.repeat(50) + '\n'));
|
|
66
|
+
// Per-checker summaries
|
|
67
|
+
for (const checkerResult of result.results) {
|
|
68
|
+
const icon = checkerResult.passed ? chalk.green('✓') : chalk.red('✗');
|
|
69
|
+
const name = checkerDisplayName(checkerResult.checker);
|
|
70
|
+
console.log(` ${icon} ${chalk.bold(name)}: ${checkerResult.summary}`);
|
|
71
|
+
// List issues
|
|
72
|
+
if (checkerResult.issues.length > 0) {
|
|
73
|
+
for (const issue of checkerResult.issues) {
|
|
74
|
+
const location = issue.line
|
|
75
|
+
? `${issue.file}:${issue.line}`
|
|
76
|
+
: issue.file;
|
|
77
|
+
const ruleTag = issue.rule ? chalk.dim(` [${issue.rule}]`) : '';
|
|
78
|
+
const patternTag = issue.pattern ? chalk.dim(` (${issue.pattern})`) : '';
|
|
79
|
+
console.log(` ${severityIcon(issue.severity)} ${chalk.dim(location)} ${severityColor(issue.severity)(issue.message)}${ruleTag}${patternTag}`);
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Errors from sub-agent failures
|
|
85
|
+
if (result.errors.length > 0) {
|
|
86
|
+
console.log(chalk.yellow(' Checker errors:'));
|
|
87
|
+
for (const err of result.errors) {
|
|
88
|
+
console.log(chalk.yellow(` ! ${err}`));
|
|
89
|
+
}
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
// Total summary
|
|
93
|
+
const totalIssues = result.results.reduce((sum, r) => sum + r.issues.length, 0);
|
|
94
|
+
const errorCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'error').length, 0);
|
|
95
|
+
const warningCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'warning').length, 0);
|
|
96
|
+
const duration = (result.duration / 1000).toFixed(1);
|
|
97
|
+
if (result.passed) {
|
|
98
|
+
console.log(chalk.green(` All checks passed (${duration}s)\n`));
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (errorCount > 0)
|
|
103
|
+
parts.push(chalk.red(`${errorCount} error(s)`));
|
|
104
|
+
if (warningCount > 0)
|
|
105
|
+
parts.push(chalk.yellow(`${warningCount} warning(s)`));
|
|
106
|
+
console.log(` ${parts.join(', ')} found across ${totalIssues} issue(s) (${duration}s)\n`);
|
|
107
|
+
}
|
|
108
|
+
// Results saved location
|
|
109
|
+
console.log(chalk.dim(' Results saved to .haystack/triage/\n'));
|
|
110
|
+
}
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Main command
|
|
113
|
+
// ============================================================================
|
|
114
|
+
export async function submitCommand(options) {
|
|
115
|
+
console.log(chalk.bold.cyan('\nHaystack Submit\n'));
|
|
116
|
+
// -------------------------------------------------------------------------
|
|
117
|
+
// Step 1: Validate environment
|
|
118
|
+
// -------------------------------------------------------------------------
|
|
119
|
+
console.log(chalk.dim('Checking environment...'));
|
|
120
|
+
// Validate mutually exclusive options
|
|
121
|
+
if (options.body && options.bodyFile) {
|
|
122
|
+
console.error(chalk.red('\nCannot use both --body and --body-file. Pick one.\n'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// Check git repo
|
|
126
|
+
const gitRoot = findGitRoot();
|
|
127
|
+
if (!gitRoot) {
|
|
128
|
+
console.error(chalk.red('\nNot a git repository.\n'));
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
// Check authentication
|
|
132
|
+
let githubToken;
|
|
133
|
+
try {
|
|
134
|
+
githubToken = await requireGitHubAuth();
|
|
135
|
+
console.log(chalk.green('✓ Authenticated'));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
// Get remote info
|
|
142
|
+
let remoteInfo;
|
|
143
|
+
try {
|
|
144
|
+
remoteInfo = parseRemoteUrl('origin');
|
|
145
|
+
console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
// Check for uncommitted changes
|
|
152
|
+
if (hasUncommittedChanges()) {
|
|
153
|
+
console.log(chalk.yellow('⚠ You have uncommitted changes. They will not be included in the PR.'));
|
|
154
|
+
}
|
|
155
|
+
// Get current branch info
|
|
156
|
+
const currentBranch = getCurrentBranch();
|
|
157
|
+
const baseBranch = options.base || getDefaultBranch();
|
|
158
|
+
console.log(chalk.dim(` Current branch: ${currentBranch}`));
|
|
159
|
+
console.log(chalk.dim(` Base branch: ${baseBranch}`));
|
|
160
|
+
// -------------------------------------------------------------------------
|
|
161
|
+
// Step 2: Pre-PR Triage
|
|
162
|
+
// -------------------------------------------------------------------------
|
|
163
|
+
if (!options.force) {
|
|
164
|
+
console.log(chalk.dim('\nRunning pre-PR triage checks...'));
|
|
165
|
+
try {
|
|
166
|
+
const triageResult = await runTriage(gitRoot, baseBranch);
|
|
167
|
+
printTriageSummary(triageResult);
|
|
168
|
+
const triageInfraFailure = triageResult.results.length === 0 && triageResult.errors.length > 0;
|
|
169
|
+
if (triageInfraFailure) {
|
|
170
|
+
console.log(chalk.yellow('\n⚠ Triage unavailable (sub-agents failed to run). Continuing with submit.\n'));
|
|
171
|
+
}
|
|
172
|
+
if (!triageResult.passed) {
|
|
173
|
+
if (triageInfraFailure) {
|
|
174
|
+
// Infrastructure failure should not block submission.
|
|
175
|
+
// We already warned above and continue.
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
console.log(chalk.red('\nTriage failed. Fix the issues above or re-run with --force.\n'));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
// Triage failure should not block submission
|
|
185
|
+
if (isCodexNetworkDisabledTriageError(err)) {
|
|
186
|
+
console.log(chalk.yellow('\n⚠ Triage skipped in Codex app shell.'));
|
|
187
|
+
console.log(chalk.dim(' Nested Codex sub-agents cannot access model endpoints from this shell.'));
|
|
188
|
+
console.log(chalk.dim(' Submit will continue normally.'));
|
|
189
|
+
console.log(chalk.dim(' For full triage, run `haystack submit` from a regular terminal.\n'));
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log(chalk.yellow(`\n⚠ Triage skipped: ${err.message}`));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log(chalk.dim('\nSkipping triage (--force)'));
|
|
198
|
+
}
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// Step 3: Check if on main/master
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
if (currentBranch === 'main' || currentBranch === 'master') {
|
|
203
|
+
console.error(chalk.red(`\nRefusing to submit from ${currentBranch}. Create a feature branch first.\n`));
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
// -------------------------------------------------------------------------
|
|
207
|
+
// Step 4: Push to remote
|
|
208
|
+
// -------------------------------------------------------------------------
|
|
209
|
+
console.log(chalk.dim('\nPushing to origin...'));
|
|
210
|
+
try {
|
|
211
|
+
pushBranch('origin', currentBranch);
|
|
212
|
+
console.log(chalk.green('✓ Branch pushed'));
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
// -------------------------------------------------------------------------
|
|
219
|
+
// Step 5: Create or find PR
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
console.log(chalk.dim('\nCreating pull request...'));
|
|
222
|
+
// Check if PR already exists
|
|
223
|
+
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch);
|
|
224
|
+
let prNumber;
|
|
225
|
+
let prUrl;
|
|
226
|
+
if (existingPR) {
|
|
227
|
+
console.log(chalk.yellow(`PR already exists: #${existingPR.number}`));
|
|
228
|
+
prNumber = existingPR.number;
|
|
229
|
+
prUrl = existingPR.html_url;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Generate PR title and body
|
|
233
|
+
const prTitle = options.title || getLastCommitMessage();
|
|
234
|
+
let bodyContent;
|
|
235
|
+
if (options.bodyFile) {
|
|
236
|
+
try {
|
|
237
|
+
bodyContent = options.bodyFile === '-'
|
|
238
|
+
? fs.readFileSync(0, 'utf-8')
|
|
239
|
+
: fs.readFileSync(options.bodyFile, 'utf-8');
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
console.error(chalk.red(`\nFailed to read body file: ${err.message}\n`));
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
bodyContent = options.body || getCommitMessagesSinceBase(baseBranch);
|
|
248
|
+
}
|
|
249
|
+
const prBody = buildPRBody(bodyContent);
|
|
250
|
+
try {
|
|
251
|
+
const pr = await createPullRequest({
|
|
252
|
+
owner: remoteInfo.owner,
|
|
253
|
+
repo: remoteInfo.repo,
|
|
254
|
+
title: prTitle,
|
|
255
|
+
head: currentBranch,
|
|
256
|
+
base: baseBranch,
|
|
257
|
+
body: prBody,
|
|
258
|
+
draft: options.draft,
|
|
259
|
+
});
|
|
260
|
+
prNumber = pr.number;
|
|
261
|
+
prUrl = pr.html_url;
|
|
262
|
+
console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
console.error(chalk.red(`\n${err.message}\n`));
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// -------------------------------------------------------------------------
|
|
270
|
+
// Step 6: Apply auto-merge label if enabled
|
|
271
|
+
// -------------------------------------------------------------------------
|
|
272
|
+
let autoMergeEnabled = false;
|
|
273
|
+
try {
|
|
274
|
+
autoMergeEnabled = await isAutoMergeEnabled();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Preference check failure should not block submission
|
|
278
|
+
}
|
|
279
|
+
if (autoMergeEnabled) {
|
|
280
|
+
try {
|
|
281
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge']);
|
|
282
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge']);
|
|
283
|
+
console.log(chalk.green('✓ Auto-merge enabled for this PR'));
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
console.log(chalk.yellow(`⚠ Could not set auto-merge label: ${err.message}`));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// -------------------------------------------------------------------------
|
|
290
|
+
// Step 7: Request review if --review
|
|
291
|
+
// -------------------------------------------------------------------------
|
|
292
|
+
if (options.review) {
|
|
293
|
+
try {
|
|
294
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review']);
|
|
295
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review']);
|
|
296
|
+
if (typeof options.review === 'string') {
|
|
297
|
+
// Specific reviewer requested
|
|
298
|
+
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review]);
|
|
299
|
+
console.log(chalk.green(`✓ Review requested from ${options.review}`));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// No reviewer specified — goes to needs-assignment queue
|
|
303
|
+
console.log(chalk.green('✓ Marked for review (needs assignment)'));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
console.log(chalk.yellow(`⚠ Could not request review: ${err.message}`));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// -------------------------------------------------------------------------
|
|
311
|
+
// Step 8: Success!
|
|
312
|
+
// -------------------------------------------------------------------------
|
|
313
|
+
console.log(chalk.green('\n' + '━'.repeat(50)));
|
|
314
|
+
console.log(chalk.bold.green('\nPull Request Created!\n'));
|
|
315
|
+
console.log(` ${chalk.dim('URL:')} ${chalk.cyan(prUrl)}`);
|
|
316
|
+
console.log(` ${chalk.dim('Title:')} ${options.title || getLastCommitMessage()}`);
|
|
317
|
+
console.log(` ${chalk.dim('Branch:')} ${currentBranch} → ${baseBranch}`);
|
|
318
|
+
if (autoMergeEnabled) {
|
|
319
|
+
console.log(` ${chalk.dim('Merge:')} ${chalk.green('Auto-merge if safe')}`);
|
|
320
|
+
}
|
|
321
|
+
if (options.review) {
|
|
322
|
+
const reviewer = typeof options.review === 'string' ? options.review : 'needs assignment';
|
|
323
|
+
console.log(` ${chalk.dim('Review:')} ${chalk.yellow(reviewer)}`);
|
|
324
|
+
}
|
|
325
|
+
console.log('');
|
|
326
|
+
// -------------------------------------------------------------------------
|
|
327
|
+
// Step 9: Wait for Haystack analysis (triggered via GitHub App webhook)
|
|
328
|
+
// -------------------------------------------------------------------------
|
|
329
|
+
console.log(chalk.dim('Haystack analysis triggered via GitHub App webhook.'));
|
|
330
|
+
// Save pending state (so check-pending works even if we disconnect)
|
|
331
|
+
const pendingState = {
|
|
332
|
+
version: 1,
|
|
333
|
+
prNumber,
|
|
334
|
+
prUrl,
|
|
335
|
+
owner: remoteInfo.owner,
|
|
336
|
+
repo: remoteInfo.repo,
|
|
337
|
+
branch: currentBranch,
|
|
338
|
+
baseBranch,
|
|
339
|
+
submittedAt: new Date().toISOString(),
|
|
340
|
+
status: 'polling',
|
|
341
|
+
};
|
|
342
|
+
savePendingSubmit(pendingState, gitRoot);
|
|
343
|
+
// If --no-wait or non-interactive, exit here
|
|
344
|
+
if (options.wait === false || !process.stdin.isTTY) {
|
|
345
|
+
console.log(chalk.dim('\nAnalysis running in background.'));
|
|
346
|
+
console.log(chalk.dim('Check results later with: ') + chalk.cyan('haystack check-pending\n'));
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Wait for analysis inline by polling the result endpoint
|
|
350
|
+
console.log(chalk.dim('\nWaiting for analysis...\n'));
|
|
351
|
+
// Handle Ctrl+C gracefully
|
|
352
|
+
let interrupted = false;
|
|
353
|
+
const sigintHandler = () => {
|
|
354
|
+
interrupted = true;
|
|
355
|
+
console.log(chalk.dim('\n\nAnalysis still running. Check later with:'));
|
|
356
|
+
console.log(chalk.cyan(' haystack check-pending\n'));
|
|
357
|
+
process.exit(0);
|
|
358
|
+
};
|
|
359
|
+
process.on('SIGINT', sigintHandler);
|
|
360
|
+
const POLL_INTERVAL = 5_000;
|
|
361
|
+
const POLL_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
362
|
+
const deadline = Date.now() + POLL_TIMEOUT;
|
|
363
|
+
let consecutiveErrors = 0;
|
|
364
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
365
|
+
while (Date.now() < deadline && !interrupted) {
|
|
366
|
+
const result = await checkAnalysisReady(remoteInfo.owner, remoteInfo.repo, prNumber, githubToken);
|
|
367
|
+
if (result.status === 'ready') {
|
|
368
|
+
consecutiveErrors = 0;
|
|
369
|
+
// Fetch results
|
|
370
|
+
try {
|
|
371
|
+
const verdict = await fetchAnalysisResults(remoteInfo.owner, remoteInfo.repo, prNumber, githubToken);
|
|
372
|
+
updatePendingSubmit({
|
|
373
|
+
status: 'completed',
|
|
374
|
+
analysisResult: verdict,
|
|
375
|
+
resolvedAt: new Date().toISOString(),
|
|
376
|
+
}, gitRoot);
|
|
377
|
+
// Print verdict
|
|
378
|
+
console.log(chalk.bold('\n' + '━'.repeat(50)));
|
|
379
|
+
console.log(chalk.bold(' Haystack Analysis Results'));
|
|
380
|
+
console.log(chalk.bold('━'.repeat(50) + '\n'));
|
|
381
|
+
if (verdict.state === 'good-to-merge') {
|
|
382
|
+
if (verdict.autoMerged) {
|
|
383
|
+
console.log(chalk.green.bold(' ✓ Auto-merged\n'));
|
|
384
|
+
console.log(chalk.dim(' No issues found. PR was automatically merged.'));
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
console.log(chalk.green.bold(' ✓ Good to merge\n'));
|
|
388
|
+
console.log(chalk.dim(` ${verdict.summary}`));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.log(chalk.yellow.bold(' ⚠ Needs your input\n'));
|
|
393
|
+
console.log(chalk.dim(` ${verdict.summary}\n`));
|
|
394
|
+
for (const issue of verdict.issues) {
|
|
395
|
+
const icon = issue.severity === 'error' ? chalk.red('✗') : chalk.yellow('!');
|
|
396
|
+
const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
|
|
397
|
+
console.log(` ${icon} ${chalk.dim(loc)} ${issue.message}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(verdict.reviewUrl)}\n`);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
console.log(chalk.yellow('⚠ Analysis complete but could not fetch results.'));
|
|
404
|
+
console.log(chalk.dim(' Check results at: ') + chalk.cyan(prUrl) + '\n');
|
|
405
|
+
}
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
if (result.status === 'error') {
|
|
409
|
+
consecutiveErrors++;
|
|
410
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
411
|
+
console.log(chalk.red(`\n Analysis check failed: ${result.message}`));
|
|
412
|
+
updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() }, gitRoot);
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
consecutiveErrors = 0;
|
|
418
|
+
}
|
|
419
|
+
process.stdout.write(`\r ${chalk.dim('Status:')} Waiting for analysis...${' '.repeat(10)}`);
|
|
420
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
421
|
+
}
|
|
422
|
+
// Clean up SIGINT handler
|
|
423
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
424
|
+
if (Date.now() >= deadline && !interrupted) {
|
|
425
|
+
console.log(chalk.yellow('\n\n Timed out waiting for analysis.'));
|
|
426
|
+
console.log(chalk.dim(' Check results later with: ') + chalk.cyan('haystack check-pending\n'));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export interface TriageOptions {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
wait?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function triageCommand(identifier: string, options: TriageOptions): Promise<void>;
|