@haystackeditor/cli 0.10.3 → 0.12.0

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