@haystackeditor/cli 0.25.1 → 0.26.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 (97) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/app-config.js +25 -1
  3. package/dist/commands/capture-brief.js +41 -32
  4. package/dist/commands/feedback.js +66 -0
  5. package/dist/commands/init-telemetry.js +53 -7
  6. package/dist/commands/init.js +7 -2
  7. package/dist/commands/lockfile-pin.js +307 -0
  8. package/dist/commands/verify.js +68 -13
  9. package/dist/index.js +27 -923
  10. package/dist/schema.js +4 -10
  11. package/dist/utils/haystack-api.js +0 -36
  12. package/package.json +1 -5
  13. package/schemas/feedback.v1.json +13 -0
  14. package/schemas/pre-verify.v2.json +240 -0
  15. package/schemas/verify-raw.v1.json +1132 -0
  16. package/schemas/verify.v2.json +655 -0
  17. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  18. package/dist/assets/hooks/agent-context/format.ts +0 -100
  19. package/dist/assets/hooks/agent-context/index.ts +0 -41
  20. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  21. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  22. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  23. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  24. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  25. package/dist/assets/hooks/agent-context/types.ts +0 -58
  26. package/dist/assets/hooks/llm-rules-template.md +0 -59
  27. package/dist/assets/hooks/package-lock.json +0 -598
  28. package/dist/assets/hooks/package.json +0 -12
  29. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  30. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  31. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  32. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  33. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  34. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  35. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  36. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  37. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  38. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  39. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  40. package/dist/assets/skills/map-your-system.md +0 -143
  41. package/dist/assets/skills/submit.md +0 -200
  42. package/dist/commands/ask.js +0 -20
  43. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  44. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  45. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  46. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  47. package/dist/commands/cloud-verifier-materialization.js +0 -704
  48. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  49. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  50. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  51. package/dist/commands/cloud-verifier-universe.js +0 -10178
  52. package/dist/commands/config.js +0 -549
  53. package/dist/commands/design-verify.js +0 -311
  54. package/dist/commands/dismiss.js +0 -159
  55. package/dist/commands/hooks.js +0 -226
  56. package/dist/commands/inbox.js +0 -137
  57. package/dist/commands/mcp.js +0 -201
  58. package/dist/commands/policy.js +0 -371
  59. package/dist/commands/pr-status.js +0 -207
  60. package/dist/commands/pr.js +0 -105
  61. package/dist/commands/prepare-universe-review.js +0 -1092
  62. package/dist/commands/production-source-deny-policy.js +0 -100
  63. package/dist/commands/request-review.js +0 -74
  64. package/dist/commands/review.js +0 -191
  65. package/dist/commands/rules.js +0 -98
  66. package/dist/commands/scaffold-provisional-universe.js +0 -806
  67. package/dist/commands/setup.js +0 -1170
  68. package/dist/commands/skills.js +0 -447
  69. package/dist/commands/submit.js +0 -745
  70. package/dist/commands/system-map.js +0 -228
  71. package/dist/commands/triage.js +0 -598
  72. package/dist/commands/webhooks.js +0 -241
  73. package/dist/states.js +0 -46
  74. package/dist/tools/detect.js +0 -832
  75. package/dist/triage/astra.js +0 -202
  76. package/dist/triage/prompts.js +0 -188
  77. package/dist/triage/runner.js +0 -200
  78. package/dist/triage/types.js +0 -7
  79. package/dist/utils/action-output.js +0 -26
  80. package/dist/utils/analysis-api.js +0 -416
  81. package/dist/utils/design-verifier-api.js +0 -294
  82. package/dist/utils/design-verifier-history.js +0 -79
  83. package/dist/utils/design-verifier-result.js +0 -424
  84. package/dist/utils/github-api.js +0 -324
  85. package/dist/utils/pending-state.js +0 -86
  86. package/dist/utils/pr-ref.js +0 -56
  87. package/dist/utils/prompter.js +0 -328
  88. package/schemas/action.v1.json +0 -22
  89. package/schemas/ask.v1.json +0 -40
  90. package/schemas/inbox.v1.json +0 -27
  91. package/schemas/pr-status.v1.json +0 -61
  92. package/schemas/pr.v1.json +0 -97
  93. package/schemas/pr.v3.json +0 -45
  94. package/schemas/setup.v1.json +0 -75
  95. package/schemas/submit.v1.json +0 -90
  96. package/schemas/triage.v1.json +0 -103
  97. package/schemas/triage.v2.json +0 -64
@@ -1,598 +0,0 @@
1
- /**
2
- * triage command — Show Haystack analysis results for a PR.
3
- *
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`).
10
- *
11
- * Accepts a PR identifier in several formats:
12
- * haystack triage # Last submitted PR
13
- * haystack triage 123 # Uses current repo's owner/repo
14
- * haystack triage owner/repo#123 # Fully qualified
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
21
- */
22
- import chalk from 'chalk';
23
- import WebSocket from 'ws';
24
- import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
25
- import { loadToken } from '../utils/auth.js';
26
- import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
27
- import { parsePrRef } from '../utils/pr-ref.js';
28
- import { toPublicVerdict } from '../states.js';
29
- import { withSchema } from '../schema.js';
30
- import { fetchGitHubJson } from '../utils/haystack-api.js';
31
- import { githubRestBaseFor } from '../utils/github-api.js';
32
- // ============================================================================
33
- // Output formatting
34
- // ============================================================================
35
- /**
36
- * True if the finding is one the auto-fixer is already handling (and thus
37
- * shouldn't be re-fixed). Correlation is by exact finding id — the same stable id
38
- * analysis assigns and the fixer echoes back in its manifest. We never compare
39
- * summary/category text.
40
- */
41
- function isAutoFixing(finding, manifest) {
42
- if (!finding.id)
43
- return false;
44
- const items = manifest?.fixedItems ?? [];
45
- return items.some((item) => item.id === finding.id);
46
- }
47
- function printRichOutput(pr, synthesis, manifest) {
48
- const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
49
- console.log(`\n ${chalk.bold(`Haystack Triage: ${pr.owner}/${pr.repo}#${pr.prNumber}`)}\n`);
50
- // Alpha output stays intentionally small. Findings remain fully visible below.
51
- if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
52
- const fixed = manifest.fixedItems.length;
53
- const remaining = manifest.leftForReview.length;
54
- console.log(chalk.dim(` Fixer: completed · fixed ${fixed}; ${remaining} remain for review.\n`));
55
- }
56
- // Every customer-facing finding stays visible regardless of fixer state.
57
- const findings = synthesis.synthesisDisplay || [];
58
- if (findings.length > 0) {
59
- console.log(chalk.bold(' Findings to address:\n'));
60
- for (let i = 0; i < findings.length; i++) {
61
- const finding = findings[i];
62
- const sourceTag = finding.source ? ` (${finding.source})` : '';
63
- console.log(` ${i + 1}. [${finding.category}]${sourceTag} ${finding.summary}`);
64
- if (finding.detail) {
65
- for (const line of finding.detail.split('\n')) {
66
- console.log(` ${line}`);
67
- }
68
- }
69
- if (finding.agentFixPrompt) {
70
- const fixLines = finding.agentFixPrompt.split('\n');
71
- console.log(` ${chalk.dim('Fix:')} ${fixLines[0]}`);
72
- for (const line of fixLines.slice(1)) {
73
- console.log(` ${line}`);
74
- }
75
- }
76
- console.log('');
77
- }
78
- }
79
- else {
80
- console.log(chalk.dim(' No findings.\n'));
81
- }
82
- console.log(` ${chalk.dim(reviewUrl)}\n`);
83
- }
84
- export function buildJsonOutput(pr, headSha, synthesis, manifest) {
85
- const payload = {
86
- ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
87
- head_sha: headSha,
88
- status: 'complete',
89
- verdict: toPublicVerdict(synthesis.analysisVerdict, synthesis.haystackRating),
90
- rating: synthesis.haystackRating,
91
- human_review_reasons: synthesis.humanReviewReasons ?? [],
92
- findings: (synthesis.synthesisDisplay || []).map((f) => ({
93
- id: f.id ?? null,
94
- category: f.category,
95
- summary: f.summary,
96
- detail: f.detail,
97
- suggested_fix: f.agentFixPrompt ?? null,
98
- source: f.source ?? null,
99
- location: f.filePath || f.startLine != null || f.endLine != null
100
- ? {
101
- path: f.filePath ?? null,
102
- start_line: f.startLine ?? null,
103
- end_line: f.endLine ?? null,
104
- }
105
- : null,
106
- severity: f.severity ?? null,
107
- })),
108
- };
109
- if (manifest) {
110
- const fixed = manifest.fixedItems.length;
111
- const remaining = manifest.leftForReview.length;
112
- payload.fixer = {
113
- status: 'completed',
114
- result_sha: manifest.resultSha ?? headSha,
115
- summary: `Fixed ${fixed} finding${fixed === 1 ? '' : 's'}; ${remaining} remain${remaining === 1 ? 's' : ''} for review.`,
116
- };
117
- }
118
- return withSchema('triage', payload);
119
- }
120
- export function buildJsonState(pr, status, message) {
121
- const payload = {
122
- ref: `${pr.owner}/${pr.repo}#${pr.prNumber}`,
123
- head_sha: null,
124
- status,
125
- verdict: null,
126
- rating: null,
127
- human_review_reasons: [],
128
- findings: [],
129
- };
130
- if (message)
131
- payload.message = message;
132
- return withSchema('triage', payload);
133
- }
134
- export async function fetchTriageHeadSha(pr, token) {
135
- if (!token)
136
- return null;
137
- const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
138
- const headSha = meta.head?.sha;
139
- if (!headSha)
140
- throw new Error('GitHub PR metadata response is missing head.sha');
141
- return headSha;
142
- }
143
- function printJsonState(pr, status, message) {
144
- console.log(JSON.stringify(buildJsonState(pr, status, message), null, 2));
145
- }
146
- function ratingStars(rating) {
147
- const filled = '\u2605'; // ★
148
- const empty = '\u2606'; // ☆
149
- return filled.repeat(rating) + empty.repeat(5 - rating);
150
- }
151
- function printHookOutput(pr, synthesis, manifest, status) {
152
- const label = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
153
- const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
154
- if (status === 'pending') {
155
- console.log(`[Haystack] ${label}: Analysis in progress...`);
156
- return;
157
- }
158
- if (status === 'failed') {
159
- console.log(`[Haystack] ${label}: Analysis failed`);
160
- return;
161
- }
162
- if (status === 'stale') {
163
- console.log(`[Haystack] ${label}: Stale (submitted > 2h ago)`);
164
- return;
165
- }
166
- if (!synthesis) {
167
- console.log(`[Haystack] ${label}: Analysis in progress...`);
168
- return;
169
- }
170
- const stars = ratingStars(synthesis.haystackRating);
171
- const fixingCount = manifest?.fixedItems?.length ?? 0;
172
- // Only count findings the auto-fixer is NOT handling \u2014 those are what need the user.
173
- const actionable = (synthesis.synthesisDisplay || []).filter((f) => !isAutoFixing(f, manifest));
174
- if (actionable.length === 0 && fixingCount === 0) {
175
- console.log(`[Haystack] \u2705 ${label} ${stars}: Good to merge`);
176
- }
177
- else if (actionable.length === 0) {
178
- // Everything is being auto-fixed \u2014 nothing for the user to do.
179
- console.log(`[Haystack] \u2705 ${label} ${stars}: ${fixingCount} being auto-fixed, nothing for you to do`);
180
- }
181
- else {
182
- const parts = [`${actionable.length} finding(s)`];
183
- if (fixingCount > 0)
184
- parts.push(`${fixingCount} being auto-fixed`);
185
- console.log(`[Haystack] \u26A0\uFE0F ${label} ${stars}: Needs your input (${parts.join(', ')})`);
186
- console.log(` ${reviewUrl}`);
187
- }
188
- }
189
- // ============================================================================
190
- // Waiting (event-driven with polling fallback)
191
- // ============================================================================
192
- const POLL_INTERVAL_MS = 5_000;
193
- const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
194
- /** With a live event subscription, checks are event-driven; this slow cycle
195
- * is only a safety net against a silently dead socket. */
196
- const SUBSCRIBED_RECHECK_MS = 60_000;
197
- const PR_EVENTS_WS_BASE = process.env.HAYSTACK_PR_EVENTS_WS_URL ?? 'wss://haystackeditor.com/api/pr-events';
198
- /**
199
- * Subscribe to the pr-events worker for this PR — the same real-time channel
200
- * the web inbox uses. Any event (analysis_complete, commit_pushed, ...) calls
201
- * `onEvent`, and the caller re-checks analysis state immediately instead of
202
- * discovering it on the next poll tick. Returns null when a socket can't even
203
- * be constructed; connection failures after that surface via isConnected().
204
- */
205
- function subscribeToPrEvents(owner, repo, prNumber, token, onEvent, quiet) {
206
- // Diagnostics are dim stderr lines, once per failure mode — polling is the
207
- // designed fallback, but WHY live updates are off must stay visible (PR001).
208
- // quiet (--hook) mode stays terse: the hook output is machine-parsed.
209
- const diag = (msg) => {
210
- if (!quiet)
211
- process.stderr.write(`\n ${chalk.dim(`[live updates unavailable: ${msg}; using polling]`)}\n`);
212
- };
213
- try {
214
- const ws = new WebSocket(`${PR_EVENTS_WS_BASE}/${owner}/${repo}/${prNumber}/subscribe`, {
215
- headers: { Authorization: `Bearer ${token}` },
216
- });
217
- let connected = false;
218
- let reported = false;
219
- ws.on('open', () => { connected = true; });
220
- ws.on('message', () => onEvent());
221
- ws.on('error', (err) => {
222
- connected = false;
223
- if (!reported) {
224
- reported = true;
225
- diag(err.message);
226
- }
227
- });
228
- ws.on('close', () => { connected = false; });
229
- const keepalive = setInterval(() => {
230
- // A failed send means the socket died between ticks; the 'error'/'close'
231
- // handlers above report it and flip isConnected() — nothing extra to say.
232
- try {
233
- ws.send('ping');
234
- }
235
- catch { /* surfaced via error/close handlers */ }
236
- }, 30_000);
237
- keepalive.unref?.();
238
- return {
239
- isConnected: () => connected,
240
- close: () => {
241
- clearInterval(keepalive);
242
- // Idempotent teardown, not a fallback: close() on an already-dead
243
- // socket can throw in ws and there is genuinely nothing to handle.
244
- try {
245
- ws.close();
246
- }
247
- catch { /* already closed */ }
248
- },
249
- };
250
- }
251
- catch (err) {
252
- diag(err instanceof Error ? err.message : String(err));
253
- return null;
254
- }
255
- }
256
- async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token, quiet) {
257
- const deadline = Date.now() + timeoutMs;
258
- let consecutiveErrors = 0;
259
- // Event-driven wake: a pr-events message resolves the current sleep so the
260
- // re-check happens the moment analysis completes, not on the next tick.
261
- let wake = null;
262
- const subscription = token
263
- ? subscribeToPrEvents(owner, repo, prNumber, token, () => wake?.(), quiet)
264
- : null;
265
- try {
266
- while (Date.now() < deadline) {
267
- const result = await checkAnalysisReady(owner, repo, prNumber, token);
268
- if (result.status === 'ready')
269
- return 'ready';
270
- if (result.status === 'error') {
271
- consecutiveErrors++;
272
- if (consecutiveErrors >= 5)
273
- return 'error';
274
- }
275
- else {
276
- consecutiveErrors = 0;
277
- }
278
- const live = subscription?.isConnected() ?? false;
279
- if (!quiet) {
280
- const mode = live ? 'Waiting for analysis (live)...' : 'Waiting for analysis to complete...';
281
- process.stderr.write(`\r ${chalk.dim(mode)}${' '.repeat(5)}`);
282
- }
283
- const interval = Math.min(live ? SUBSCRIBED_RECHECK_MS : POLL_INTERVAL_MS, Math.max(deadline - Date.now(), 0));
284
- await new Promise((resolve) => {
285
- const timer = setTimeout(() => { wake = null; resolve(); }, interval);
286
- timer.unref?.();
287
- wake = () => { clearTimeout(timer); wake = null; resolve(); };
288
- });
289
- }
290
- return 'pending';
291
- }
292
- finally {
293
- subscription?.close();
294
- }
295
- }
296
- // ============================================================================
297
- // PR resolution
298
- // ============================================================================
299
- /**
300
- * Resolve a PR from an explicit identifier or from pending-submit state.
301
- * Returns null (and handles output) if no PR can be resolved.
302
- */
303
- function resolvePR(identifier, options) {
304
- if (identifier) {
305
- try {
306
- return parsePrRef(identifier, 'triage');
307
- }
308
- catch (err) {
309
- console.error(chalk.red(`\n${err.message}\n`));
310
- process.exit(1);
311
- }
312
- }
313
- // No arg — read from pending state
314
- const pending = loadPendingSubmit();
315
- if (!pending) {
316
- if (options.json) {
317
- // Versioned error envelope (the old `{"status":"none"}` matched no
318
- // schema); the caller asked for a triage and there is nothing to triage.
319
- console.log(JSON.stringify(withSchema('error', {
320
- status: 'error',
321
- error: 'No pending submit to triage. Pass a PR identifier: haystack triage owner/repo#123 --json',
322
- }), null, 2));
323
- process.exitCode = 1;
324
- }
325
- else if (!options.hook) {
326
- console.log(chalk.dim('No pending submits. Pass a PR number to triage a specific PR.\n'));
327
- }
328
- return null;
329
- }
330
- return { owner: pending.owner, repo: pending.repo, prNumber: pending.prNumber };
331
- }
332
- // ============================================================================
333
- // Main command
334
- // ============================================================================
335
- export async function triageCommand(identifier, options) {
336
- // Handle --clear
337
- if (options.clear) {
338
- clearPendingSubmit();
339
- if (options.json) {
340
- // stdout stays pure JSON in --json mode; use the shared action envelope.
341
- console.log(JSON.stringify(withSchema('action', {
342
- action: 'clear_pending',
343
- status: 'ok',
344
- ref: null,
345
- detail: 'Pending submit state cleared.',
346
- }), null, 2));
347
- }
348
- else if (!options.hook) {
349
- console.log(chalk.dim('Pending submit state cleared.'));
350
- }
351
- return;
352
- }
353
- // Resolve PR identifier — explicit arg or fall back to pending-submit.json
354
- const pr = resolvePR(identifier, options);
355
- if (!pr)
356
- return;
357
- const isQuiet = options.hook || options.json;
358
- // Load GitHub token for private repo auth
359
- let token;
360
- try {
361
- token = await loadToken({ owner: pr.owner, repo: pr.repo }) || undefined;
362
- }
363
- catch (err) {
364
- if (!isQuiet) {
365
- console.log(chalk.yellow(`\n⚠ Could not load Haystack credentials: ${err.message}`));
366
- console.log(chalk.dim(' Continuing without GitHub auth. Private repos may require `haystack login`.\n'));
367
- }
368
- }
369
- if (!isQuiet) {
370
- console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
371
- }
372
- // Check if analysis is ready — resolve to a terminal state before fetching
373
- let analysisReady = false;
374
- const readyResult = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
375
- if (readyResult.status === 'ready') {
376
- analysisReady = true;
377
- }
378
- else if (readyResult.status === 'pending') {
379
- // Hook mode: short 5s poll. --no-wait: skip. Default: full poll.
380
- if (options.hook) {
381
- const hookResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, 5_000, token, true);
382
- if (hookResult === 'ready') {
383
- analysisReady = true;
384
- }
385
- else {
386
- updatePendingSubmitIfExists(pr);
387
- printHookOutput(pr, null, null, 'pending');
388
- return;
389
- }
390
- }
391
- else if (options.wait === false) {
392
- if (options.json) {
393
- printJsonState(pr, 'pending');
394
- }
395
- else {
396
- console.log(chalk.yellow('\n Analysis is still in progress.'));
397
- console.log(chalk.dim(' Re-run without --no-wait to poll until complete.\n'));
398
- }
399
- return;
400
- }
401
- else {
402
- // Full poll
403
- if (!options.json) {
404
- console.log(chalk.dim(' Analysis in progress, waiting...'));
405
- }
406
- const waitResult = await waitForAnalysis(pr.owner, pr.repo, pr.prNumber, POLL_TIMEOUT_MS, token, options.json);
407
- if (!options.json) {
408
- process.stderr.write('\r' + ' '.repeat(60) + '\r');
409
- }
410
- if (waitResult === 'ready') {
411
- analysisReady = true;
412
- }
413
- else if (waitResult === 'pending') {
414
- if (options.json) {
415
- printJsonState(pr, 'pending', 'Timed out waiting for analysis');
416
- }
417
- else {
418
- console.log(chalk.yellow('\n Timed out waiting for analysis.'));
419
- console.log(chalk.dim(' Try again later: ') + chalk.cyan(`haystack triage ${pr.owner}/${pr.repo}#${pr.prNumber}\n`));
420
- }
421
- return;
422
- }
423
- else {
424
- // waitResult === 'error'
425
- if (options.json) {
426
- printJsonState(pr, 'failed', 'Analysis check failed');
427
- }
428
- else {
429
- console.error(chalk.red('\n Analysis check failed.\n'));
430
- }
431
- process.exit(1);
432
- }
433
- }
434
- }
435
- else {
436
- // readyResult.status === 'error'
437
- if (options.hook) {
438
- printHookOutput(pr, null, null, 'failed');
439
- return;
440
- }
441
- if (options.json) {
442
- printJsonState(pr, 'failed', readyResult.message);
443
- }
444
- else {
445
- console.error(chalk.red(`\n Analysis check failed: ${readyResult.message}\n`));
446
- }
447
- process.exit(1);
448
- }
449
- if (!analysisReady)
450
- return;
451
- // Fetch the full rating synthesis and auto-fix manifest in parallel.
452
- // Auth failures throw a classified error — report it as failed (with the
453
- // remediation) rather than the old behavior of "not available yet, retry".
454
- let synthesis;
455
- let autoFixManifest;
456
- let headSha;
457
- try {
458
- [synthesis, autoFixManifest, headSha] = await Promise.all([
459
- fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
460
- fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
461
- fetchTriageHeadSha(pr, token),
462
- ]);
463
- }
464
- catch (err) {
465
- const message = err instanceof Error ? err.message : String(err);
466
- if (options.hook) {
467
- // Session-start hooks must never fail the session — match the
468
- // status-check error path above: print the failed line, exit 0.
469
- printHookOutput(pr, null, null, 'failed');
470
- return;
471
- }
472
- if (options.json) {
473
- printJsonState(pr, 'failed', message);
474
- }
475
- else {
476
- console.error(chalk.red(`\n ${message}\n`));
477
- }
478
- process.exit(1);
479
- }
480
- // Update pending state if this PR matches the pending submit
481
- updatePendingSubmitIfExists(pr, synthesis ? 'completed' : undefined);
482
- if (!synthesis) {
483
- if (options.hook) {
484
- printHookOutput(pr, null, null, 'pending');
485
- return;
486
- }
487
- if (options.json) {
488
- printJsonState(pr, 'unavailable', 'Rating synthesis not available');
489
- }
490
- else {
491
- console.error(chalk.yellow('\n Analysis completed but rating synthesis is not available yet.'));
492
- console.log(chalk.dim(' Try again in a moment, or view the full review:'));
493
- console.log(chalk.cyan(` https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
494
- }
495
- process.exit(1);
496
- }
497
- // Output
498
- if (options.hook) {
499
- printHookOutput(pr, synthesis, autoFixManifest);
500
- }
501
- else if (options.json) {
502
- console.log(JSON.stringify(buildJsonOutput(pr, headSha, synthesis, autoFixManifest), null, 2));
503
- }
504
- else {
505
- printRichOutput(pr, synthesis, autoFixManifest);
506
- }
507
- // Record findings-seen marker (fire-and-forget — failure is non-fatal)
508
- if (synthesis.synthesisDisplay && synthesis.synthesisDisplay.length > 0 && token) {
509
- const findingKeys = synthesis.synthesisDisplay.map((f) => `${f.category}|${f.summary}`);
510
- recordFindingsSeen(pr, token, findingKeys).catch((err) => {
511
- // Non-blocking but logged — don't swallow silently
512
- if (process.env.DEBUG) {
513
- console.error(chalk.dim(` [debug] findings-seen write failed: ${err}`));
514
- }
515
- });
516
- }
517
- }
518
- /** Pure read path for MCP: returns a public payload without writing stdout or exiting. */
519
- export async function fetchTriagePayload(identifier) {
520
- const pr = parsePrRef(identifier, 'triage');
521
- const token = await loadToken({ owner: pr.owner, repo: pr.repo }) || undefined;
522
- const ready = await checkAnalysisReady(pr.owner, pr.repo, pr.prNumber, token);
523
- if (ready.status === 'pending')
524
- return buildJsonState(pr, 'pending');
525
- if (ready.status === 'error')
526
- return buildJsonState(pr, 'failed', ready.message);
527
- const [synthesis, manifest, headSha] = await Promise.all([
528
- fetchRatingSynthesis(pr.owner, pr.repo, pr.prNumber, token),
529
- fetchAutoFixManifest(pr.owner, pr.repo, pr.prNumber, token),
530
- fetchTriageHeadSha(pr, token),
531
- ]);
532
- if (!synthesis)
533
- return buildJsonState(pr, 'unavailable', 'Rating synthesis not available');
534
- return buildJsonOutput(pr, headSha, synthesis, manifest);
535
- }
536
- /**
537
- * Update the pending-submit state if the given PR matches the pending one.
538
- * Best-effort — failure is silently ignored.
539
- */
540
- function updatePendingSubmitIfExists(pr, status) {
541
- try {
542
- const pending = loadPendingSubmit();
543
- if (!pending)
544
- return;
545
- if (pending.owner !== pr.owner || pending.repo !== pr.repo || pending.prNumber !== pr.prNumber)
546
- return;
547
- if (pending.status !== 'polling')
548
- return;
549
- if (status === 'completed') {
550
- updatePendingSubmit({
551
- status: 'completed',
552
- resolvedAt: new Date().toISOString(),
553
- });
554
- }
555
- }
556
- catch {
557
- // Non-fatal
558
- }
559
- }
560
- /**
561
- * Record that the user has seen analysis findings for this PR.
562
- * Fire-and-forget: failure is silently ignored.
563
- */
564
- async function recordFindingsSeen(pr, token, findingKeys) {
565
- // Get PR head SHA
566
- const ghResp = await fetch(`${githubRestBaseFor(token)}/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, {
567
- headers: {
568
- Authorization: `Bearer ${token}`,
569
- Accept: 'application/vnd.github.v3+json',
570
- 'User-Agent': 'Haystack-CLI',
571
- },
572
- });
573
- if (!ghResp.ok) {
574
- throw new Error(`GitHub API returned ${ghResp.status}`);
575
- }
576
- const ghData = await ghResp.json();
577
- const sha = ghData?.head?.sha;
578
- if (typeof sha !== 'string' || !sha) {
579
- throw new Error('Unexpected GitHub response: missing head.sha');
580
- }
581
- const resp = await fetch('https://haystackeditor.com/api/findings-seen', {
582
- method: 'POST',
583
- headers: {
584
- Authorization: `Bearer ${token}`,
585
- 'Content-Type': 'application/json',
586
- 'User-Agent': 'Haystack-CLI',
587
- },
588
- body: JSON.stringify({
589
- repoFullName: `${pr.owner}/${pr.repo}`,
590
- prNumber: pr.prNumber,
591
- commitSha: sha,
592
- findingKeys,
593
- }),
594
- });
595
- if (!resp.ok) {
596
- throw new Error(`findings-seen API returned ${resp.status}`);
597
- }
598
- }