@haystackeditor/cli 0.25.0 → 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 (113) hide show
  1. package/README.md +39 -455
  2. package/dist/capture/adapters/django.js +7 -0
  3. package/dist/capture/adapters/index.js +10 -0
  4. package/dist/capture/adapters/next-build.js +113 -0
  5. package/dist/capture/adapters/next.js +272 -105
  6. package/dist/capture/adapters/nuxt.js +7 -0
  7. package/dist/capture/adapters/rails.js +7 -0
  8. package/dist/capture/adapters/react-router.js +7 -0
  9. package/dist/capture/adapters/sveltekit.js +7 -0
  10. package/dist/capture/adapters/vite.js +14 -0
  11. package/dist/capture/app-config.js +27 -2
  12. package/dist/capture/csp.js +7 -6
  13. package/dist/capture/url-rewrites.js +232 -0
  14. package/dist/capture-step.js +3 -0
  15. package/dist/commands/capture-brief.js +41 -32
  16. package/dist/commands/capture-manifest.js +25 -17
  17. package/dist/commands/feedback.js +66 -0
  18. package/dist/commands/init-capture.js +44 -27
  19. package/dist/commands/init-telemetry.js +77 -14
  20. package/dist/commands/init.js +19 -4
  21. package/dist/commands/lockfile-pin.js +307 -0
  22. package/dist/commands/verify-onboarding.js +16 -0
  23. package/dist/commands/verify.js +70 -15
  24. package/dist/index.js +29 -923
  25. package/dist/schema.js +4 -10
  26. package/dist/utils/haystack-api.js +0 -36
  27. package/package.json +1 -5
  28. package/schemas/feedback.v1.json +13 -0
  29. package/schemas/init.v1.json +1 -1
  30. package/schemas/pre-verify.v2.json +240 -0
  31. package/schemas/verify-raw.v1.json +1132 -0
  32. package/schemas/verify.v2.json +655 -0
  33. package/dist/assets/hooks/agent-context/detect.ts +0 -316
  34. package/dist/assets/hooks/agent-context/format.ts +0 -100
  35. package/dist/assets/hooks/agent-context/index.ts +0 -41
  36. package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
  37. package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
  38. package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
  39. package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
  40. package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
  41. package/dist/assets/hooks/agent-context/types.ts +0 -58
  42. package/dist/assets/hooks/llm-rules-template.md +0 -59
  43. package/dist/assets/hooks/package-lock.json +0 -598
  44. package/dist/assets/hooks/package.json +0 -12
  45. package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
  46. package/dist/assets/hooks/scripts/post-commit.sh +0 -5
  47. package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
  48. package/dist/assets/hooks/scripts/pre-push.sh +0 -25
  49. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
  50. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
  51. package/dist/assets/hooks/truncation-checker/index.ts +0 -595
  52. package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
  53. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
  54. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
  55. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
  56. package/dist/assets/skills/map-your-system.md +0 -143
  57. package/dist/assets/skills/submit.md +0 -200
  58. package/dist/commands/ask.js +0 -20
  59. package/dist/commands/cloud-verifier-behaviors.js +0 -218
  60. package/dist/commands/cloud-verifier-data-store-census.js +0 -539
  61. package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
  62. package/dist/commands/cloud-verifier-identity-census.js +0 -4060
  63. package/dist/commands/cloud-verifier-materialization.js +0 -704
  64. package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
  65. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
  66. package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
  67. package/dist/commands/cloud-verifier-universe.js +0 -10178
  68. package/dist/commands/config.js +0 -549
  69. package/dist/commands/design-verify.js +0 -311
  70. package/dist/commands/dismiss.js +0 -159
  71. package/dist/commands/hooks.js +0 -226
  72. package/dist/commands/inbox.js +0 -137
  73. package/dist/commands/mcp.js +0 -201
  74. package/dist/commands/policy.js +0 -371
  75. package/dist/commands/pr-status.js +0 -207
  76. package/dist/commands/pr.js +0 -105
  77. package/dist/commands/prepare-universe-review.js +0 -1092
  78. package/dist/commands/production-source-deny-policy.js +0 -100
  79. package/dist/commands/request-review.js +0 -74
  80. package/dist/commands/review.js +0 -191
  81. package/dist/commands/rules.js +0 -98
  82. package/dist/commands/scaffold-provisional-universe.js +0 -806
  83. package/dist/commands/setup.js +0 -1170
  84. package/dist/commands/skills.js +0 -447
  85. package/dist/commands/submit.js +0 -745
  86. package/dist/commands/system-map.js +0 -228
  87. package/dist/commands/triage.js +0 -598
  88. package/dist/commands/webhooks.js +0 -241
  89. package/dist/states.js +0 -46
  90. package/dist/tools/detect.js +0 -832
  91. package/dist/triage/astra.js +0 -202
  92. package/dist/triage/prompts.js +0 -188
  93. package/dist/triage/runner.js +0 -200
  94. package/dist/triage/types.js +0 -7
  95. package/dist/utils/action-output.js +0 -26
  96. package/dist/utils/analysis-api.js +0 -416
  97. package/dist/utils/design-verifier-api.js +0 -294
  98. package/dist/utils/design-verifier-history.js +0 -79
  99. package/dist/utils/design-verifier-result.js +0 -424
  100. package/dist/utils/github-api.js +0 -324
  101. package/dist/utils/pending-state.js +0 -86
  102. package/dist/utils/pr-ref.js +0 -56
  103. package/dist/utils/prompter.js +0 -328
  104. package/schemas/action.v1.json +0 -22
  105. package/schemas/ask.v1.json +0 -40
  106. package/schemas/inbox.v1.json +0 -27
  107. package/schemas/pr-status.v1.json +0 -61
  108. package/schemas/pr.v1.json +0 -97
  109. package/schemas/pr.v3.json +0 -45
  110. package/schemas/setup.v1.json +0 -75
  111. package/schemas/submit.v1.json +0 -90
  112. package/schemas/triage.v1.json +0 -103
  113. 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
- }