@haystackeditor/cli 0.11.0 → 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.
@@ -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
23
  import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
14
24
  import { parseRemoteUrl } from '../utils/git.js';
15
25
  import { loadToken } from './login.js';
26
+ import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
16
27
  // ============================================================================
17
28
  // PR identifier parsing
18
29
  // ============================================================================
@@ -57,209 +68,120 @@ function parsePRIdentifier(identifier) {
57
68
  // ============================================================================
58
69
  // Output formatting
59
70
  // ============================================================================
60
- function ratingLabel(rating) {
61
- switch (rating) {
62
- case 5: return chalk.green.bold('5/5') + chalk.green(' — Safe to merge');
63
- case 4: return chalk.green('4/5') + chalk.dim(' — Low risk');
64
- case 3: return chalk.yellow('3/5') + chalk.dim(' — Moderate risk');
65
- case 2: return chalk.red('2/5') + chalk.dim(' — High risk');
66
- case 1: return chalk.red.bold('1/5') + chalk.red(' — Critical issues');
67
- default: return chalk.dim(`${rating}/5`);
68
- }
69
- }
70
- function verdictLabel(verdict) {
71
- switch (verdict) {
72
- case 'clean': return chalk.green.bold('Clean');
73
- case 'has-issues': return chalk.yellow.bold('Has Issues');
74
- default: return chalk.dim('Unknown');
75
- }
76
- }
77
- function severityIcon(severity) {
78
- switch (severity) {
79
- case 'critical': return chalk.red.bold('✗✗');
80
- case 'high': return chalk.red('✗');
81
- case 'medium': return chalk.yellow('!');
82
- case 'low': return chalk.blue('·');
83
- default: return chalk.dim('-');
84
- }
85
- }
86
71
  function printRichOutput(pr, synthesis, manifest) {
87
72
  const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
88
- console.log(chalk.bold('\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
- // Auto-fix manifest — show what the fixer did vs left for human review
73
+ console.log(`\n ${chalk.bold(`Haystack Triage: ${pr.owner}/${pr.repo}#${pr.prNumber}`)}\n`);
74
+ // Auto-fixer status — what's being fixed vs what it skipped
121
75
  if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
122
- console.log(chalk.bold('\n Auto-Fixer Results\n'));
123
76
  if (manifest.fixedItems.length > 0) {
124
- console.log(` ${chalk.green.bold(`Fixed (${manifest.fixedItems.length})`)}`);
77
+ console.log(chalk.bold(' Auto-fixer is handling:\n'));
125
78
  for (const item of manifest.fixedItems) {
126
- console.log(` ${chalk.green(' ✓')} ${chalk.dim(`[${item.category}]`)} ${item.summary}`);
79
+ console.log(` - [${item.category}] ${item.summary}`);
127
80
  }
128
81
  console.log('');
129
82
  }
130
83
  if (manifest.leftForReview.length > 0) {
131
- console.log(` ${chalk.yellow.bold(`Left for you (${manifest.leftForReview.length})`)}`);
84
+ console.log(chalk.bold(' Auto-fixer skipped:\n'));
132
85
  for (const item of manifest.leftForReview) {
133
- console.log(` ${chalk.yellow(' →')} ${chalk.dim(`[${item.category}]`)} ${item.summary}`);
134
- console.log(` ${chalk.dim(item.reason)}`);
86
+ console.log(` - [${item.category}] ${item.summary}`);
87
+ console.log(` ${chalk.dim(item.reason)}`);
135
88
  }
136
89
  console.log('');
137
90
  }
138
91
  }
139
- // Findings (synthesisDisplay) split into new and previously reported
140
- if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0) {
141
- const newFindings = synthesis.synthesisDisplay.filter((f) => f.isNew !== false);
142
- const knownFindings = synthesis.synthesisDisplay.filter((f) => f.isNew === false);
143
- // New findings shown prominently
144
- if (newFindings.length > 0) {
145
- console.log(chalk.bold('\n Findings\n'));
146
- for (let i = 0; i < newFindings.length; i++) {
147
- const finding = newFindings[i];
148
- const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
149
- console.log(` ${chalk.yellow(`${i + 1}.`)} ${chalk.bold(finding.category)}${sourceTag}`);
150
- console.log(` ${finding.summary}`);
151
- if (finding.detail) {
152
- const detailLines = finding.detail.split('\n');
153
- for (const line of detailLines) {
154
- console.log(` ${chalk.dim(line)}`);
155
- }
92
+ // Findings — the actionable output for agents
93
+ const findings = synthesis.synthesisDisplay || [];
94
+ if (findings.length > 0) {
95
+ console.log(chalk.bold(' Findings:\n'));
96
+ for (let i = 0; i < findings.length; i++) {
97
+ const finding = findings[i];
98
+ const sourceTag = finding.source ? ` (${finding.source})` : '';
99
+ console.log(` ${i + 1}. [${finding.category}]${sourceTag} ${finding.summary}`);
100
+ if (finding.detail) {
101
+ for (const line of finding.detail.split('\n')) {
102
+ console.log(` ${line}`);
156
103
  }
157
- console.log('');
158
- }
159
- }
160
- // Previously reported findings — dimmed
161
- if (knownFindings.length > 0) {
162
- console.log(chalk.dim(` Previously Reported (${knownFindings.length})\n`));
163
- for (let i = 0; i < knownFindings.length; i++) {
164
- const finding = knownFindings[i];
165
- const sourceTag = finding.source ? chalk.dim(` (via ${finding.source})`) : '';
166
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.dim(finding.category)}${sourceTag}`);
167
- console.log(` ${chalk.dim(finding.summary)}`);
168
- console.log('');
169
104
  }
170
- }
171
- }
172
- // Verified bugs
173
- const confirmedBugs = (synthesis.verifiedBugs || []).filter(b => b.verified);
174
- if (confirmedBugs.length > 0) {
175
- console.log(chalk.bold(' Verified Bugs\n'));
176
- for (const bug of confirmedBugs) {
177
- const sourceTag = bug.source ? chalk.dim(` (via ${bug.source.type}${bug.source.author ? `: ${bug.source.author}` : ''})`) : '';
178
- console.log(` ${severityIcon(bug.assessedSeverity)} ${chalk.bold(bug.originalTitle)}${sourceTag}`);
179
- console.log(` ${chalk.dim('Severity:')} ${bug.assessedSeverity}${bug.likelihood ? `, ${bug.likelihood} occurrence` : ''}`);
180
- console.log(` ${bug.rationale}`);
181
- console.log('');
182
- }
183
- }
184
- // Human review reasons
185
- if (synthesis.humanReviewReasons && synthesis.humanReviewReasons.length > 0) {
186
- console.log(chalk.bold(' Human Review Required\n'));
187
- for (const reason of synthesis.humanReviewReasons) {
188
- const icon = reason.severity === 'high' || reason.severity === 'critical'
189
- ? chalk.red('!')
190
- : chalk.yellow('·');
191
- console.log(` ${icon} ${reason.reason}`);
192
- if (reason.source) {
193
- console.log(` ${chalk.dim(`Source: ${reason.source}`)}`);
105
+ if (finding.agentFixPrompt) {
106
+ const fixLines = finding.agentFixPrompt.split('\n');
107
+ console.log(` ${chalk.dim('Fix:')} ${fixLines[0]}`);
108
+ for (const line of fixLines.slice(1)) {
109
+ console.log(` ${line}`);
110
+ }
194
111
  }
195
- }
196
- console.log('');
197
- }
198
- // Agent fix prompts — only for new findings (known issues were already triaged)
199
- const fixableFindings = (synthesis.synthesisDisplay || []).filter((f) => f.agentFixPrompt && f.isNew !== false);
200
- if (fixableFindings.length > 0) {
201
- console.log(chalk.bold(' Agent Fix Prompts'));
202
- console.log(chalk.dim(' (Copy these to your coding agent to fix the issues)\n'));
203
- for (let i = 0; i < fixableFindings.length; i++) {
204
- const finding = fixableFindings[i];
205
- console.log(chalk.cyan(` --- Fix ${i + 1}: ${finding.category} ---`));
206
- console.log(` ${finding.agentFixPrompt}`);
207
112
  console.log('');
208
113
  }
209
114
  }
210
- // Verification impact
211
- if (synthesis.verificationImpact) {
212
- console.log(` ${chalk.dim('Verification impact:')} ${synthesis.verificationImpact}`);
213
- console.log('');
115
+ else if (!manifest || manifest.fixedItems.length === 0) {
116
+ console.log(chalk.dim(' No findings.\n'));
214
117
  }
215
- console.log(` ${chalk.dim('Full review:')} ${chalk.cyan(reviewUrl)}`);
216
- console.log('');
118
+ console.log(` ${chalk.dim(reviewUrl)}\n`);
217
119
  }
218
120
  function printJsonOutput(pr, synthesis, manifest) {
219
- const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
220
121
  const output = {
221
122
  owner: pr.owner,
222
123
  repo: pr.repo,
223
124
  prNumber: pr.prNumber,
224
- haystackRating: synthesis.haystackRating,
225
- analysisVerdict: synthesis.analysisVerdict,
226
- phase: synthesis.phase,
227
- canAutoMerge: synthesis.reviewRecommendation?.canAutoMerge ?? false,
228
- needsHumanReview: synthesis.needsHumanReview,
125
+ rating: synthesis.haystackRating,
126
+ autoFixerHandling: manifest?.fixedItems ?? [],
127
+ autoFixerSkipped: manifest?.leftForReview ?? [],
229
128
  findings: (synthesis.synthesisDisplay || []).map((f) => ({
230
129
  category: f.category,
231
130
  summary: f.summary,
232
131
  detail: f.detail,
233
132
  agentFixPrompt: f.agentFixPrompt || null,
234
133
  source: f.source || null,
235
- isNew: f.isNew ?? null,
236
134
  })),
237
- verifiedBugs: (synthesis.verifiedBugs || []).filter(b => b.verified).map(b => ({
238
- title: b.originalTitle,
239
- severity: b.assessedSeverity,
240
- likelihood: b.likelihood,
241
- rationale: b.rationale,
242
- source: b.source || null,
243
- })),
244
- humanReviewReasons: synthesis.humanReviewReasons || [],
245
- verification: synthesis.verification || null,
246
- verificationConfidence: synthesis.verificationConfidence || null,
247
- autoFixManifest: manifest ? {
248
- fixedItems: manifest.fixedItems,
249
- leftForReview: manifest.leftForReview,
250
- timestamp: manifest.timestamp,
251
- } : null,
252
- reviewUrl,
253
135
  };
254
136
  console.log(JSON.stringify(output, null, 2));
255
137
  }
138
+ function ratingStars(rating) {
139
+ const filled = '\u2605'; // ★
140
+ const empty = '\u2606'; // ☆
141
+ return filled.repeat(rating) + empty.repeat(5 - rating);
142
+ }
143
+ function printHookOutput(pr, synthesis, manifest, status) {
144
+ const label = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
145
+ const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
146
+ if (status === 'pending') {
147
+ console.log(`[Haystack] ${label}: Analysis in progress...`);
148
+ return;
149
+ }
150
+ if (status === 'failed') {
151
+ console.log(`[Haystack] ${label}: Analysis failed`);
152
+ return;
153
+ }
154
+ if (status === 'stale') {
155
+ console.log(`[Haystack] ${label}: Stale (submitted > 2h ago)`);
156
+ return;
157
+ }
158
+ if (!synthesis) {
159
+ console.log(`[Haystack] ${label}: Analysis in progress...`);
160
+ return;
161
+ }
162
+ const findings = synthesis.synthesisDisplay || [];
163
+ const stars = ratingStars(synthesis.haystackRating);
164
+ const fixingCount = manifest?.fixedItems?.length ?? 0;
165
+ if (findings.length === 0 && fixingCount === 0) {
166
+ console.log(`[Haystack] \u2705 ${label} ${stars}: Good to merge`);
167
+ }
168
+ else {
169
+ const parts = [];
170
+ if (findings.length > 0)
171
+ parts.push(`${findings.length} finding(s)`);
172
+ if (fixingCount > 0)
173
+ parts.push(`${fixingCount} being auto-fixed`);
174
+ console.log(`[Haystack] \u26A0\uFE0F ${label} ${stars}: Needs your input (${parts.join(', ')})`);
175
+ console.log(` ${reviewUrl}`);
176
+ }
177
+ }
256
178
  // ============================================================================
257
179
  // Polling
258
180
  // ============================================================================
259
181
  const POLL_INTERVAL_MS = 5_000;
260
182
  const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
261
- async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
262
- const deadline = Date.now() + POLL_TIMEOUT_MS;
183
+ async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token, quiet) {
184
+ const deadline = Date.now() + timeoutMs;
263
185
  let consecutiveErrors = 0;
264
186
  while (Date.now() < deadline) {
265
187
  const result = await checkAnalysisReady(owner, repo, prNumber, token);
@@ -281,21 +203,55 @@ async function waitForAnalysis(owner, repo, prNumber, token, quiet) {
281
203
  return 'pending';
282
204
  }
283
205
  // ============================================================================
206
+ // PR resolution
207
+ // ============================================================================
208
+ /**
209
+ * Resolve a PR from an explicit identifier or from pending-submit state.
210
+ * Returns null (and handles output) if no PR can be resolved.
211
+ */
212
+ function resolvePR(identifier, options) {
213
+ if (identifier) {
214
+ try {
215
+ return parsePRIdentifier(identifier);
216
+ }
217
+ catch (err) {
218
+ console.error(chalk.red(`\n${err.message}\n`));
219
+ process.exit(1);
220
+ }
221
+ }
222
+ // No arg — read from pending state
223
+ const pending = loadPendingSubmit();
224
+ if (!pending) {
225
+ if (options.json) {
226
+ console.log(JSON.stringify({ status: 'none' }));
227
+ }
228
+ else if (!options.hook) {
229
+ console.log(chalk.dim('No pending submits. Pass a PR number to triage a specific PR.\n'));
230
+ }
231
+ return null;
232
+ }
233
+ return { owner: pending.owner, repo: pending.repo, prNumber: pending.prNumber };
234
+ }
235
+ // ============================================================================
284
236
  // Main command
285
237
  // ============================================================================
286
238
  export async function triageCommand(identifier, options) {
287
- // Parse PR identifier
288
- let pr;
289
- try {
290
- pr = parsePRIdentifier(identifier);
291
- }
292
- catch (err) {
293
- console.error(chalk.red(`\n${err.message}\n`));
294
- process.exit(1);
239
+ // Handle --clear
240
+ if (options.clear) {
241
+ clearPendingSubmit();
242
+ if (!options.hook) {
243
+ console.log(chalk.dim('Pending submit state cleared.'));
244
+ }
245
+ return;
295
246
  }
247
+ // Resolve PR identifier — explicit arg or fall back to pending-submit.json
248
+ const pr = resolvePR(identifier, options);
249
+ if (!pr)
250
+ return;
296
251
  // Load GitHub token for private repo auth
297
252
  const token = await loadToken() || undefined;
298
- if (!options.json) {
253
+ const isQuiet = options.hook || options.json;
254
+ if (!isQuiet) {
299
255
  console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
300
256
  }
301
257
  // Check if analysis is ready — resolve to a terminal state before fetching
@@ -305,56 +261,74 @@ export async function triageCommand(identifier, options) {
305
261
  analysisReady = true;
306
262
  }
307
263
  else if (readyResult.status === 'pending') {
308
- if (options.wait === false) {
309
- if (options.json) {
310
- console.log(JSON.stringify({
311
- owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
312
- }, 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;
313
269
  }
314
270
  else {
315
- console.log(chalk.yellow('\n Analysis is still in progress.'));
316
- 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;
317
274
  }
318
- return;
319
275
  }
320
- // Wait for analysis
321
- if (!options.json) {
322
- console.log(chalk.dim(' Analysis in progress, waiting...'));
323
- }
324
- const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, token, options.json);
325
- if (!options.json) {
326
- process.stderr.write('\r' + ' '.repeat(60) + '\r');
327
- }
328
- if (waitResult === 'ready') {
329
- analysisReady = true;
330
- }
331
- else if (waitResult === 'pending') {
276
+ else if (options.wait === false) {
332
277
  if (options.json) {
333
278
  console.log(JSON.stringify({
334
279
  owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'pending',
335
280
  }, null, 2));
336
281
  }
337
282
  else {
338
- console.log(chalk.yellow('\n Timed out waiting for analysis.'));
339
- 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'));
340
285
  }
341
286
  return;
342
287
  }
343
288
  else {
344
- // waitResult === 'error'
345
- if (options.json) {
346
- console.log(JSON.stringify({
347
- owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber, status: 'error',
348
- }, 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;
349
311
  }
350
312
  else {
351
- 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);
352
323
  }
353
- process.exit(1);
354
324
  }
355
325
  }
356
326
  else {
357
327
  // readyResult.status === 'error'
328
+ if (options.hook) {
329
+ printHookOutput(pr, null, null, 'failed');
330
+ return;
331
+ }
358
332
  if (options.json) {
359
333
  console.log(JSON.stringify({
360
334
  owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
@@ -373,7 +347,13 @@ export async function triageCommand(identifier, options) {
373
347
  fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
374
348
  fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
375
349
  ]);
350
+ // Update pending state if this PR matches the pending submit
351
+ updatePendingSubmitIfExists(pr, synthesis ? 'completed' : undefined);
376
352
  if (!synthesis) {
353
+ if (options.hook) {
354
+ printHookOutput(pr, null, null, 'pending');
355
+ return;
356
+ }
377
357
  if (options.json) {
378
358
  console.log(JSON.stringify({
379
359
  owner: pr.owner, repo: pr.repo, prNumber: pr.prNumber,
@@ -388,7 +368,10 @@ export async function triageCommand(identifier, options) {
388
368
  process.exit(1);
389
369
  }
390
370
  // Output
391
- if (options.json) {
371
+ if (options.hook) {
372
+ printHookOutput(pr, synthesis, autoFixManifest);
373
+ }
374
+ else if (options.json) {
392
375
  printJsonOutput(pr, synthesis, autoFixManifest);
393
376
  }
394
377
  else {
@@ -405,6 +388,30 @@ export async function triageCommand(identifier, options) {
405
388
  });
406
389
  }
407
390
  }
391
+ /**
392
+ * Update the pending-submit state if the given PR matches the pending one.
393
+ * Best-effort — failure is silently ignored.
394
+ */
395
+ function updatePendingSubmitIfExists(pr, status) {
396
+ try {
397
+ const pending = loadPendingSubmit();
398
+ if (!pending)
399
+ return;
400
+ if (pending.owner !== pr.owner || pending.repo !== pr.repo || pending.prNumber !== pr.prNumber)
401
+ return;
402
+ if (pending.status !== 'polling')
403
+ return;
404
+ if (status === 'completed') {
405
+ updatePendingSubmit({
406
+ status: 'completed',
407
+ resolvedAt: new Date().toISOString(),
408
+ });
409
+ }
410
+ }
411
+ catch {
412
+ // Non-fatal
413
+ }
414
+ }
408
415
  /**
409
416
  * Record that the user has seen analysis findings for this PR.
410
417
  * Fire-and-forget: failure is silently ignored.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  * npx @haystackeditor/cli status # Check configuration
12
12
  * npx @haystackeditor/cli login # Authenticate with GitHub
13
13
  * npx @haystackeditor/cli submit # Create a PR (auto-merge or review)
14
- * npx @haystackeditor/cli 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