@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.
Files changed (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
@@ -0,0 +1,354 @@
1
+ /**
2
+ * triage command — View Haystack analysis results for any PR.
3
+ *
4
+ * Fetches the full rating synthesis (same data shown on the Haystack feed)
5
+ * and displays verdict, findings, verified bugs, and agent fix prompts.
6
+ *
7
+ * Accepts a PR identifier in several formats:
8
+ * haystack triage 123 # Uses current repo's owner/repo
9
+ * haystack triage owner/repo#123 # Fully qualified
10
+ * haystack triage https://github.com/owner/repo/pull/123
11
+ */
12
+ import chalk from 'chalk';
13
+ import { checkAnalysisReady, fetchRatingSynthesis } from '../utils/analysis-api.js';
14
+ import { parseRemoteUrl } from '../utils/git.js';
15
+ import { loadToken } from './login.js';
16
+ // ============================================================================
17
+ // PR identifier parsing
18
+ // ============================================================================
19
+ /**
20
+ * Parse a PR identifier from various formats:
21
+ * 123 -> uses current repo
22
+ * #123 -> uses current repo
23
+ * owner/repo#123 -> fully qualified
24
+ * https://github.com/owner/repo/pull/123 -> GitHub URL
25
+ */
26
+ function parsePRIdentifier(identifier) {
27
+ // GitHub URL: https://github.com/owner/repo/pull/123
28
+ const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
29
+ if (urlMatch) {
30
+ return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
31
+ }
32
+ // Fully qualified: owner/repo#123
33
+ const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
34
+ if (qualifiedMatch) {
35
+ return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
36
+ }
37
+ // Bare number: 123 or #123
38
+ const numberMatch = identifier.match(/^#?(\d+)$/);
39
+ if (numberMatch) {
40
+ const prNumber = parseInt(numberMatch[1], 10);
41
+ // Infer owner/repo from git remote
42
+ try {
43
+ const remote = parseRemoteUrl('origin');
44
+ return { owner: remote.owner, repo: remote.repo, prNumber };
45
+ }
46
+ catch {
47
+ throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
48
+ `Use the full format: haystack triage owner/repo#${prNumber}`);
49
+ }
50
+ }
51
+ throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
52
+ `Accepted formats:\n` +
53
+ ` haystack triage 123\n` +
54
+ ` haystack triage owner/repo#123\n` +
55
+ ` haystack triage https://github.com/owner/repo/pull/123`);
56
+ }
57
+ // ============================================================================
58
+ // Output formatting
59
+ // ============================================================================
60
+ function ratingLabel(rating) {
61
+ switch (rating) {
62
+ case 5: return chalk.green.bold('5/5') + chalk.green(' — Safe to merge');
63
+ case 4: return chalk.green('4/5') + chalk.dim(' — Low risk');
64
+ case 3: return chalk.yellow('3/5') + chalk.dim(' — Moderate risk');
65
+ case 2: return chalk.red('2/5') + chalk.dim(' — High risk');
66
+ case 1: return chalk.red.bold('1/5') + chalk.red(' — Critical issues');
67
+ default: return chalk.dim(`${rating}/5`);
68
+ }
69
+ }
70
+ function verdictLabel(verdict) {
71
+ switch (verdict) {
72
+ case 'clean': return chalk.green.bold('Clean');
73
+ case 'has-issues': return chalk.yellow.bold('Has Issues');
74
+ default: return chalk.dim('Unknown');
75
+ }
76
+ }
77
+ function severityIcon(severity) {
78
+ switch (severity) {
79
+ case 'critical': return chalk.red.bold('✗✗');
80
+ case 'high': return chalk.red('✗');
81
+ case 'medium': return chalk.yellow('!');
82
+ case 'low': return chalk.blue('·');
83
+ default: return chalk.dim('-');
84
+ }
85
+ }
86
+ function printRichOutput(pr, synthesis) {
87
+ const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
88
+ console.log(chalk.bold('\n' + '━'.repeat(60)));
89
+ console.log(chalk.bold(` Haystack Analysis: ${pr.owner}/${pr.repo}#${pr.prNumber}`));
90
+ console.log(chalk.bold('━'.repeat(60) + '\n'));
91
+ // Rating & verdict
92
+ console.log(` ${chalk.dim('Rating:')} ${ratingLabel(synthesis.haystackRating)}`);
93
+ console.log(` ${chalk.dim('Verdict:')} ${verdictLabel(synthesis.analysisVerdict)}`);
94
+ if (synthesis.phase) {
95
+ const phaseLabel = synthesis.phase === 2 ? 'Final (post-verification)' : 'Preliminary (pre-verification)';
96
+ console.log(` ${chalk.dim('Phase:')} ${chalk.dim(phaseLabel)}`);
97
+ }
98
+ if (synthesis.verificationConfidence) {
99
+ const conf = synthesis.verificationConfidence;
100
+ const levelColor = conf.level === 'high' ? chalk.green : conf.level === 'medium' ? chalk.yellow : chalk.red;
101
+ console.log(` ${chalk.dim('Verified:')} ${levelColor(conf.level)} — ${chalk.dim(conf.summary)}`);
102
+ }
103
+ if (synthesis.reviewRecommendation?.canAutoMerge) {
104
+ console.log(` ${chalk.dim('Merge:')} ${chalk.green('Safe to auto-merge')}`);
105
+ }
106
+ // Verification results (build/test/lint)
107
+ if (synthesis.verification) {
108
+ const v = synthesis.verification;
109
+ const parts = [];
110
+ if (v.build)
111
+ parts.push(v.build.success ? chalk.green('build ✓') : chalk.red('build ✗'));
112
+ if (v.test)
113
+ parts.push(v.test.success ? chalk.green('test ✓') : chalk.red('test ✗'));
114
+ if (v.lint)
115
+ parts.push(v.lint.success ? chalk.green('lint ✓') : chalk.red('lint ✗'));
116
+ if (parts.length > 0) {
117
+ console.log(` ${chalk.dim('Checks:')} ${parts.join(' ')}`);
118
+ }
119
+ }
120
+ // Findings (synthesisDisplay) — the main actionable content
121
+ if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0) {
122
+ console.log(chalk.bold('\n Findings\n'));
123
+ for (let i = 0; i < synthesis.synthesisDisplay.length; i++) {
124
+ const finding = synthesis.synthesisDisplay[i];
125
+ const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
126
+ console.log(` ${chalk.yellow(`${i + 1}.`)} ${chalk.bold(finding.category)}${sourceTag}`);
127
+ console.log(` ${finding.summary}`);
128
+ if (finding.detail) {
129
+ // Indent multi-line detail
130
+ const detailLines = finding.detail.split('\n');
131
+ for (const line of detailLines) {
132
+ console.log(` ${chalk.dim(line)}`);
133
+ }
134
+ }
135
+ console.log('');
136
+ }
137
+ }
138
+ // Verified bugs
139
+ const confirmedBugs = (synthesis.verifiedBugs || []).filter(b => b.verified);
140
+ if (confirmedBugs.length > 0) {
141
+ console.log(chalk.bold(' Verified Bugs\n'));
142
+ for (const bug of confirmedBugs) {
143
+ const sourceTag = bug.source ? chalk.dim(` (via ${bug.source.type}${bug.source.author ? `: ${bug.source.author}` : ''})`) : '';
144
+ console.log(` ${severityIcon(bug.assessedSeverity)} ${chalk.bold(bug.originalTitle)}${sourceTag}`);
145
+ console.log(` ${chalk.dim('Severity:')} ${bug.assessedSeverity}${bug.likelihood ? `, ${bug.likelihood} occurrence` : ''}`);
146
+ console.log(` ${bug.rationale}`);
147
+ console.log('');
148
+ }
149
+ }
150
+ // Human review reasons
151
+ if (synthesis.humanReviewReasons && synthesis.humanReviewReasons.length > 0) {
152
+ console.log(chalk.bold(' Human Review Required\n'));
153
+ for (const reason of synthesis.humanReviewReasons) {
154
+ const icon = reason.severity === 'high' || reason.severity === 'critical'
155
+ ? chalk.red('!')
156
+ : chalk.yellow('·');
157
+ console.log(` ${icon} ${reason.reason}`);
158
+ if (reason.source) {
159
+ console.log(` ${chalk.dim(`Source: ${reason.source}`)}`);
160
+ }
161
+ }
162
+ console.log('');
163
+ }
164
+ // Agent fix prompts — the key actionable output for coding agents
165
+ const fixableFindings = (synthesis.synthesisDisplay || []).filter(f => f.agentFixPrompt);
166
+ if (fixableFindings.length > 0) {
167
+ console.log(chalk.bold(' Agent Fix Prompts'));
168
+ console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
169
+ for (let i = 0; i < fixableFindings.length; i++) {
170
+ const finding = fixableFindings[i];
171
+ console.log(chalk.cyan(` --- Fix ${i + 1}: ${finding.category} ---`));
172
+ console.log(` ${finding.agentFixPrompt}`);
173
+ console.log('');
174
+ }
175
+ }
176
+ // Verification impact
177
+ if (synthesis.verificationImpact) {
178
+ console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
179
+ console.log('');
180
+ }
181
+ console.log(` ${chalk.dim('Full review:')} ${chalk.cyan(reviewUrl)}`);
182
+ console.log('');
183
+ }
184
+ function printJsonOutput(pr, synthesis) {
185
+ const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
186
+ const output = {
187
+ owner: pr.owner,
188
+ repo: pr.repo,
189
+ prNumber: pr.prNumber,
190
+ haystackRating: synthesis.haystackRating,
191
+ analysisVerdict: synthesis.analysisVerdict,
192
+ phase: synthesis.phase,
193
+ canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
194
+ needsHumanReview: synthesis.needsHumanReview,
195
+ findings: (synthesis.synthesisDisplay || []).map(f => ({
196
+ category: f.category,
197
+ summary: f.summary,
198
+ detail: f.detail,
199
+ agentFixPrompt: f.agentFixPrompt || null,
200
+ source: f.source || null,
201
+ })),
202
+ verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
203
+ title: b.originalTitle,
204
+ severity: b.assessedSeverity,
205
+ likelihood: b.likelihood,
206
+ rationale: b.rationale,
207
+ source: b.source || null,
208
+ })),
209
+ humanReviewReasons: synthesis.humanReviewReasons || [],
210
+ verification: synthesis.verification || null,
211
+ verificationConfidence: synthesis.verificationConfidence || null,
212
+ reviewUrl,
213
+ };
214
+ console.log(JSON.stringify(output, null, 2));
215
+ }
216
+ // ============================================================================
217
+ // Polling
218
+ // ============================================================================
219
+ const POLL_INTERVAL_MS = 5_000;
220
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
221
+ async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
222
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
223
+ let consecutiveErrors = 0;
224
+ while (Date.now() < deadline) {
225
+ const result = await checkAnalysisReady(owner, repo, prNumber, token);
226
+ if (result.status === 'ready')
227
+ return 'ready';
228
+ if (result.status === 'error') {
229
+ consecutiveErrors++;
230
+ if (consecutiveErrors >= 5)
231
+ return 'error';
232
+ }
233
+ else {
234
+ consecutiveErrors = 0;
235
+ }
236
+ if (!quiet) {
237
+ process.stderr.write(`\r ${chalk.dim('Waiting for analysis to complete...')}${' '.repeat(5)}`);
238
+ }
239
+ await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
240
+ }
241
+ return 'pending';
242
+ }
243
+ // ============================================================================
244
+ // Main command
245
+ // ============================================================================
246
+ export async function triageCommand(identifier, options) {
247
+ // Parse PR identifier
248
+ let pr;
249
+ try {
250
+ pr = parsePRIdentifier(identifier);
251
+ }
252
+ catch (err) {
253
+ console.error(chalk.red(`\n${err.message}\n`));
254
+ process.exit(1);
255
+ }
256
+ // Load GitHub token for private repo auth
257
+ const token = await loadToken() || undefined;
258
+ if (!options.json) {
259
+ console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
260
+ }
261
+ // Check if analysis is ready — resolve to a terminal state before fetching
262
+ let analysisReady = false;
263
+ const readyResult = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
264
+ if (readyResult.status === 'ready') {
265
+ analysisReady = true;
266
+ }
267
+ else if (readyResult.status === 'pending') {
268
+ if (options.wait === false) {
269
+ if (options.json) {
270
+ console.log(JSON.stringify({
271
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
272
+ }, null, 2));
273
+ }
274
+ else {
275
+ console.log(chalk.yellow('\n Analysis is still in progress.'));
276
+ console.log(chalk.dim(' Re-run without --no-wait to poll until complete.\n'));
277
+ }
278
+ return;
279
+ }
280
+ // Wait for analysis
281
+ if (!options.json) {
282
+ console.log(chalk.dim(' Analysis in progress, waiting...'));
283
+ }
284
+ const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, token, options.json);
285
+ if (!options.json) {
286
+ process.stderr.write('\r' + ' '.repeat(60) + '\r');
287
+ }
288
+ if (waitResult === 'ready') {
289
+ analysisReady = true;
290
+ }
291
+ else if (waitResult === 'pending') {
292
+ if (options.json) {
293
+ console.log(JSON.stringify({
294
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
295
+ }, null, 2));
296
+ }
297
+ else {
298
+ console.log(chalk.yellow('\n Timed out waiting for analysis.'));
299
+ console.log(chalk.dim(' Try again later: ') + chalk.cyan(`haystack triage ${pr.owner}/${pr.repo}#${pr.prNumber}\n`));
300
+ }
301
+ return;
302
+ }
303
+ else {
304
+ // waitResult === 'error'
305
+ if (options.json) {
306
+ console.log(JSON.stringify({
307
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
308
+ }, null, 2));
309
+ }
310
+ else {
311
+ console.error(chalk.red('\n Analysis check failed.\n'));
312
+ }
313
+ process.exit(1);
314
+ }
315
+ }
316
+ else {
317
+ // readyResult.status === 'error'
318
+ if (options.json) {
319
+ console.log(JSON.stringify({
320
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
321
+ status: 'error', message: readyResult.message,
322
+ }, null, 2));
323
+ }
324
+ else {
325
+ console.error(chalk.red(`\n Analysis check failed: ${readyResult.message}\n`));
326
+ }
327
+ process.exit(1);
328
+ }
329
+ if (!analysisReady)
330
+ return;
331
+ // Fetch the full rating synthesis
332
+ const synthesis = await fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token);
333
+ if (!synthesis) {
334
+ if (options.json) {
335
+ console.log(JSON.stringify({
336
+ owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
337
+ status: 'error', message: 'Rating synthesis not available',
338
+ }, null, 2));
339
+ }
340
+ else {
341
+ console.error(chalk.yellow('\n Analysis completed but rating synthesis is not available yet.'));
342
+ console.log(chalk.dim(' Try again in a moment, or view the full review:'));
343
+ console.log(chalk.cyan(` https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
344
+ }
345
+ process.exit(1);
346
+ }
347
+ // Output
348
+ if (options.json) {
349
+ printJsonOutput(pr, synthesis);
350
+ }
351
+ else {
352
+ printRichOutput(pr, synthesis);
353
+ }
354
+ }
package/dist/index.d.ts CHANGED
@@ -9,6 +9,9 @@
9
9
  * npx @haystackeditor/cli init # Set up .haystack.json
10
10
  * npx @haystackeditor/cli status # Check configuration
11
11
  * npx @haystackeditor/cli login # Authenticate with GitHub
12
+ * npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
13
+ * npx @haystackeditor/cli check-pending # Check analysis status
14
+ * npx @haystackeditor/cli triage 123 # View analysis results for a PR
12
15
  * npx @haystackeditor/cli secrets list # List stored secrets
13
16
  */
14
17
  export {};