@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.
Files changed (55) hide show
  1. package/README.md +46 -0
  2. package/dist/assets/hooks/llm-rules-template.md +21 -0
  3. package/dist/assets/hooks/scripts/pre-push.sh +20 -0
  4. package/dist/assets/skills/prepare-haystack.md +323 -0
  5. package/dist/assets/skills/secrets.md +164 -0
  6. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  7. package/dist/assets/skills/setup-haystack.md +639 -0
  8. package/dist/assets/skills/submit.md +154 -0
  9. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  10. package/dist/assets/templates/haystack.yml +193 -0
  11. package/dist/commands/check-pending.d.ts +19 -0
  12. package/dist/commands/check-pending.js +217 -0
  13. package/dist/commands/config.d.ts +13 -21
  14. package/dist/commands/config.js +278 -92
  15. package/dist/commands/install-session-hooks.d.ts +16 -0
  16. package/dist/commands/install-session-hooks.js +302 -0
  17. package/dist/commands/login.js +1 -1
  18. package/dist/commands/policy.d.ts +31 -0
  19. package/dist/commands/policy.js +365 -0
  20. package/dist/commands/skills.d.ts +2 -2
  21. package/dist/commands/skills.js +51 -186
  22. package/dist/commands/submit.d.ts +22 -0
  23. package/dist/commands/submit.js +428 -0
  24. package/dist/commands/triage.d.ts +16 -0
  25. package/dist/commands/triage.js +354 -0
  26. package/dist/index.d.ts +3 -0
  27. package/dist/index.js +232 -2
  28. package/dist/tools/detect.d.ts +50 -0
  29. package/dist/tools/detect.js +853 -0
  30. package/dist/tools/fixtures.d.ts +38 -0
  31. package/dist/tools/fixtures.js +199 -0
  32. package/dist/tools/setup.d.ts +43 -0
  33. package/dist/tools/setup.js +597 -0
  34. package/dist/triage/prompts.d.ts +31 -0
  35. package/dist/triage/prompts.js +266 -0
  36. package/dist/triage/runner.d.ts +21 -0
  37. package/dist/triage/runner.js +325 -0
  38. package/dist/triage/traces.d.ts +20 -0
  39. package/dist/triage/traces.js +305 -0
  40. package/dist/triage/types.d.ts +47 -0
  41. package/dist/triage/types.js +7 -0
  42. package/dist/types.d.ts +1387 -191
  43. package/dist/types.js +254 -2
  44. package/dist/utils/analysis-api.d.ts +108 -0
  45. package/dist/utils/analysis-api.js +194 -0
  46. package/dist/utils/config.js +1 -1
  47. package/dist/utils/git.d.ts +80 -0
  48. package/dist/utils/git.js +302 -0
  49. package/dist/utils/github-api.d.ts +83 -0
  50. package/dist/utils/github-api.js +266 -0
  51. package/dist/utils/pending-state.d.ts +38 -0
  52. package/dist/utils/pending-state.js +86 -0
  53. package/dist/utils/secrets.js +3 -3
  54. package/dist/utils/skill.js +257 -0
  55. package/package.json +4 -2
@@ -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>;