@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,416 +0,0 @@
1
- /**
2
- * Haystack Analysis API client.
3
- *
4
- * Polls the result endpoint by PR identifier to check if analysis is ready,
5
- * then fetches detailed results (bug detection + rule violations).
6
- *
7
- * Analysis is triggered automatically by the GitHub App webhook when a PR
8
- * is created or updated — the CLI does not need to trigger it directly.
9
- */
10
- import { classifyHttpError } from './haystack-api.js';
11
- // ============================================================================
12
- // Constants
13
- // ============================================================================
14
- /**
15
- * Analysis API base.
16
- *
17
- * IMPORTANT: this used to point directly at AWS API Gateway. That worked
18
- * when the CLI carried a GitHub token (X-GitHub-Token header to the
19
- * Lambda), but BROKE for `haystack login --headless` users — an
20
- * hsk_live_* token would land on AWS as if it were a GH token, the
21
- * Lambda would 401, and triage / pr / chat / MCP would silently return
22
- * nothing. The right move is to route reads through the auth-worker,
23
- * which:
24
- *
25
- * 1. Knows how to introspect hsk_live tokens (and GH tokens, agent JWTs,
26
- * session cookies) — single normalized auth path.
27
- * 2. Applies the hsk_live scope gate (denyForCliScope) before forwarding
28
- * to AWS, so a repo-scoped CI token can't read other repos' synthesis.
29
- * 3. Adds the right backend token (install token for org-installed reads,
30
- * fallback otherwise) — so private-repo reads work without the CLI
31
- * ever holding a GH token.
32
- *
33
- * Override with HAYSTACK_API_BASE for local dev (e.g., http://localhost:8788
34
- * pointing at a local auth-worker).
35
- */
36
- const ANALYSIS_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
37
- /** Build the auth-worker URL for a v3 result file (rating_synthesis.json, etc). */
38
- function v3ResultUrl(owner, repo, prNumber, file) {
39
- // The auth-worker exposes `/api/analysis/{prIdentifier}/{file}` (see
40
- // V3_API_PATTERNS in infra/auth-worker/index.js). prIdentifier format:
41
- // owner/repo#prnum, URL-encoded as %2F + %23.
42
- const prId = encodeURIComponent(`${owner}/${repo}#${prNumber}`);
43
- return `${ANALYSIS_API_BASE}/api/analysis/${prId}${file ? `/${file}` : ''}`;
44
- }
45
- // ============================================================================
46
- // API helpers
47
- // ============================================================================
48
- /**
49
- * Build headers for an auth-worker-proxied analysis request.
50
- *
51
- * Auth-worker accepts hsk_live_*, GH tokens, agent JWTs, and session
52
- * cookies on `Authorization: Bearer`. The legacy `X-GitHub-Token` header
53
- * targeted AWS directly and is no longer used.
54
- */
55
- function apiHeaders(token) {
56
- const headers = {
57
- 'Cache-Control': 'no-cache',
58
- 'User-Agent': 'Haystack-CLI',
59
- };
60
- if (token) {
61
- headers['Authorization'] = `Bearer ${token}`;
62
- }
63
- return headers;
64
- }
65
- /**
66
- * Trigger a fresh full analysis for a PR via the auth-worker
67
- * (`/api/analysis-trigger/{owner}/{repo}/{pr}`). Requires auth — triggers
68
- * start a real analysis run. Returns the request id to wait on.
69
- */
70
- export async function triggerAnalysis(owner, repo, prNumber, token, headSha) {
71
- const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/${owner}/${repo}/${prNumber}`;
72
- let response;
73
- try {
74
- // Force semantics ride on a server-set header at the trigger route.
75
- // commitSha matters for dedup: without it, ingestion matches this
76
- // request onto any in-flight run for an OLDER head and the CLI would
77
- // wait on (and render) stale results.
78
- response = await fetch(url, {
79
- method: 'POST',
80
- headers: { ...apiHeaders(token), 'Content-Type': 'application/json' },
81
- body: JSON.stringify(headSha ? { commitSha: headSha } : {}),
82
- });
83
- }
84
- catch (err) {
85
- return { status: 'error', message: err.message };
86
- }
87
- let data = {};
88
- let parseFailed = false;
89
- try {
90
- data = await response.json();
91
- }
92
- catch {
93
- // Recorded, not swallowed: for non-OK responses the status-code handling
94
- // below surfaces the failure; a 2xx with an unparseable body is its own
95
- // explicit error (never "empty data") so callers can't mistake a
96
- // malformed response for a normal one.
97
- parseFailed = true;
98
- }
99
- if (parseFailed && response.ok) {
100
- return { status: 'error', message: `Trigger endpoint returned invalid JSON (HTTP ${response.status})` };
101
- }
102
- if (response.status === 429) {
103
- return {
104
- status: 'rate_limited',
105
- message: data.message || 'Too many analysis requests. Please wait before trying again.',
106
- retryAfterSeconds: data.retryAfter,
107
- };
108
- }
109
- if (response.status === 401 || response.status === 403) {
110
- return {
111
- status: 'denied',
112
- message: data.message || data.error || `HTTP ${response.status}`,
113
- };
114
- }
115
- if (!response.ok) {
116
- return { status: 'error', message: data.message || data.error || `HTTP ${response.status}` };
117
- }
118
- if (!data.request_id) {
119
- return { status: 'error', message: 'Trigger response did not include a request id' };
120
- }
121
- if (data.status === 'already_processing') {
122
- return { status: 'already_processing', requestId: data.request_id };
123
- }
124
- return { status: 'started', requestId: data.request_id };
125
- }
126
- // Terminal states of the v3 status vocabulary (map_status_for_frontend in
127
- // haystack-v3-request-status). Everything else — QUEUED, PROCESSING,
128
- // CLONING, PREPARING, ANALYZING, not_found (row lag), and any state a newer
129
- // backend introduces — reads as pending: misclassifying a healthy
130
- // in-progress state as an error would abort the wait.
131
- const TERMINAL_STATUSES = {
132
- COMPLETED: 'COMPLETED',
133
- FAILED: 'FAILED',
134
- TIMED_OUT: 'FAILED',
135
- SUPERSEDED: 'SUPERSEDED',
136
- };
137
- /**
138
- * Poll the status of a triggered run by request id. The PR-level result
139
- * endpoint can't distinguish a fresh re-run on the same head from the
140
- * previous completed analysis — the request id can.
141
- */
142
- export async function fetchTriggerStatus(requestId, token) {
143
- const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/status/${requestId}`;
144
- try {
145
- const response = await fetch(url, { headers: apiHeaders(token) });
146
- // The status Lambda returns not_found as a 404 WITH a JSON body — parse
147
- // before rejecting on status code so "not yet written" is distinguishable
148
- // from a real transport failure.
149
- let data = {};
150
- try {
151
- data = await response.json();
152
- }
153
- catch {
154
- return { status: 'error', message: `HTTP ${response.status}` };
155
- }
156
- const status = typeof data.status === 'string' ? data.status : '';
157
- const terminal = TERMINAL_STATUSES[status];
158
- if (terminal) {
159
- return { status: terminal, message: data.message };
160
- }
161
- // Only successful responses (plus the documented 404 not_found row-lag
162
- // case) read as pending — a 5xx carrying {"status":"error"} must count
163
- // toward the error budget, not spin the wait for the full timeout.
164
- if (response.ok || (response.status === 404 && status === 'not_found')) {
165
- return { status: 'pending', message: data.message };
166
- }
167
- return { status: 'error', message: data.message || `HTTP ${response.status}` };
168
- }
169
- catch (err) {
170
- return { status: 'error', message: err.message };
171
- }
172
- }
173
- /**
174
- * Check if analysis results are available for a PR by polling the result endpoint.
175
- * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
176
- */
177
- export async function checkAnalysisReady(owner, repo, prNumber, token) {
178
- const url = v3ResultUrl(owner, repo, prNumber);
179
- let response;
180
- try {
181
- response = await fetch(url, {
182
- headers: apiHeaders(token),
183
- });
184
- }
185
- catch (err) {
186
- return { status: 'error', message: err.message };
187
- }
188
- if (response.status === 404)
189
- return { status: 'pending' };
190
- if (!response.ok) {
191
- // Classified, actionable message — a bare "HTTP 403" used to send agents
192
- // into a retry loop when the fix was `haystack login`.
193
- return { status: 'error', message: (await classifyHttpError(response, 'Analysis status check')).message };
194
- }
195
- let data;
196
- try {
197
- data = await response.json();
198
- }
199
- catch {
200
- return { status: 'error', message: 'Invalid JSON response' };
201
- }
202
- return data.status === 'COMPLETED' ? { status: 'ready' } : { status: 'pending' };
203
- }
204
- /**
205
- * Fetch analysis results once complete. Used by `haystack submit`'s
206
- * wait-for-triage loop to render a verdict block.
207
- *
208
- * Verdict semantics — three distinct signals, never conflated:
209
- *
210
- * state = is the analysis itself clean?
211
- * → 'good-to-merge' iff analysisVerdict === 'clean'
212
- * AND !needsHumanReview. canAutoMerge is NOT
213
- * the source of truth (it only means "analysis
214
- * would let this through" — review policy,
215
- * grace period, or a missing auto-merge label
216
- * can still block).
217
- * Falls back to haystackRating ≥ 5 only when
218
- * analysisVerdict is absent (legacy payloads).
219
- * needsHumanReview = preserved on the return shape so callers can
220
- * distinguish "clean + needs human" from clean.
221
- * autoMerged = was the PR ACTUALLY merged? Read from the
222
- * GitHub REST endpoint, not from anything in
223
- * the synthesis. canAutoMerge is eligibility,
224
- * not outcome — never claim "auto-merged" from
225
- * the analysis side.
226
- *
227
- * Sources from the v3 rating-synthesis path (auth-worker-proxied — so
228
- * hsk_live tokens, GH tokens, agent JWTs, and cookies all work, and the
229
- * scope gate runs). The legacy v2 bug-detection / rule-violation /
230
- * merge-decision JSONs are no longer queried (no auth-worker proxy).
231
- */
232
- async function isPrMerged(owner, repo, prNumber, token) {
233
- // Authoritative merge check. We hit the same auth-worker proxy as the
234
- // rest of the CLI so hsk_live / OAuth / install tokens all work, AND
235
- // scope enforcement happens here too (a token not scoped to this repo
236
- // would 403; we treat that as "unknown → not merged" rather than
237
- // crashing the submit flow).
238
- if (!token)
239
- return false;
240
- try {
241
- const base = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
242
- const res = await fetch(`${base}/api/github/repos/${owner}/${repo}/pulls/${prNumber}`, {
243
- headers: {
244
- Accept: 'application/vnd.github+json',
245
- Authorization: `Bearer ${token}`,
246
- 'User-Agent': 'Haystack-CLI',
247
- },
248
- });
249
- if (!res.ok)
250
- return false;
251
- const data = (await res.json());
252
- return data.merged === true;
253
- }
254
- catch {
255
- return false;
256
- }
257
- }
258
- export async function fetchAnalysisResults(owner, repo, prNumber, token) {
259
- const reviewUrl = `https://haystackeditor.com/review/${owner}/${repo}/${prNumber}`;
260
- const [synthesis, autoMerged] = await Promise.all([
261
- fetchRatingSynthesis(owner, repo, prNumber, token),
262
- isPrMerged(owner, repo, prNumber, token),
263
- ]);
264
- if (!synthesis) {
265
- return {
266
- state: 'needs-input',
267
- bugCount: 0,
268
- warningCount: 0,
269
- ruleViolationCount: 0,
270
- summary: 'Analysis is ready but synthesis could not be loaded.',
271
- issues: [],
272
- reviewUrl,
273
- autoMerged,
274
- };
275
- }
276
- const display = synthesis.synthesisDisplay ?? [];
277
- const issues = display.map((d) => ({
278
- file: 'unknown', // synthesis findings don't always carry a path; the rich data lives on the feed
279
- message: d.summary || d.detail || `Finding (${d.category})`,
280
- severity: 'warning',
281
- // Map the synthesis source onto the verdict's two-bucket model.
282
- // Anything other than an explicit rule violation lands on bug-detection.
283
- source: d.source === 'rule-violation' ? 'rule-violation' : 'bug-detection',
284
- }));
285
- const bugCount = issues.filter((i) => i.source === 'bug-detection').length;
286
- const ruleViolationCount = issues.filter((i) => i.source === 'rule-violation').length;
287
- const warningCount = issues.length;
288
- // Three discrete signals, NEVER conflated:
289
- // verdictKind : 'clean' | 'has-issues' | 'needs-review'
290
- // (legacy payloads without analysisVerdict fall back
291
- // to haystackRating ≥ 5 → 'clean')
292
- // needsHumanReview: true if review policy demands a human, OR if the
293
- // orchestrator escalated verdict to 'needs-review'
294
- // (which carries the same semantic).
295
- // analysisIsClean : true iff verdict is 'clean' OR 'needs-review'
296
- // (both mean: no bugs/violations were found; the
297
- // review-policy gate is the separate axis).
298
- //
299
- // good-to-merge = analysis clean AND no human required.
300
- // 'has-issues' → state 'needs-input'.
301
- // 'needs-review' → state 'needs-input' + needsHumanReview true.
302
- // 'clean' + needsHumanReview → same as 'needs-review' (defensive — the
303
- // orchestrator should already have escalated, but we handle the case
304
- // where it didn't).
305
- const verdictKind = synthesis.analysisVerdict
306
- ?? (synthesis.haystackRating >= 5 ? 'clean' : 'has-issues');
307
- const analysisIsClean = verdictKind === 'clean' || verdictKind === 'needs-review';
308
- const needsHumanReview = synthesis.needsHumanReview === true || verdictKind === 'needs-review';
309
- const isGoodToMerge = analysisIsClean && !needsHumanReview;
310
- const state = isGoodToMerge ? 'good-to-merge' : 'needs-input';
311
- let summary;
312
- if (state === 'good-to-merge') {
313
- summary = autoMerged
314
- ? 'No blocking issues. PR was merged.'
315
- : 'No blocking issues. Safe to merge.';
316
- }
317
- else if (analysisIsClean && needsHumanReview) {
318
- summary = 'Analysis is clean, but review policy requires a human reviewer.';
319
- }
320
- else {
321
- const parts = [];
322
- if (bugCount > 0)
323
- parts.push(`${bugCount} finding(s)`);
324
- if (ruleViolationCount > 0)
325
- parts.push(`${ruleViolationCount} rule violation(s)`);
326
- summary = parts.length > 0
327
- ? parts.join(', ') + ' found.'
328
- : `Rating ${synthesis.haystackRating}/5 — see the feed for details.`;
329
- }
330
- return {
331
- state,
332
- bugCount,
333
- warningCount,
334
- ruleViolationCount,
335
- summary,
336
- issues,
337
- reviewUrl,
338
- autoMerged,
339
- needsHumanReview,
340
- };
341
- }
342
- /**
343
- * Null means "artifact does not exist (yet)" — 404s and transient
344
- * network/parse failures. Auth failures (401/403) THROW a classified
345
- * HaystackApiError instead: collapsing them into null used to make an
346
- * expired token read as "not available yet — try again in a moment",
347
- * sending agents into a retry loop when the fix was `haystack login`.
348
- */
349
- export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
350
- const url = v3ResultUrl(owner, repo, prNumber, 'auto-fix-manifest.json');
351
- try {
352
- const response = await fetch(url, {
353
- headers: apiHeaders(token),
354
- });
355
- if (response.status === 401 || response.status === 403) {
356
- throw await classifyHttpError(response, 'Auto-fix manifest fetch');
357
- }
358
- if (!response.ok)
359
- return null;
360
- const data = await response.json();
361
- if (data.download_url) {
362
- const s3Resp = await fetch(data.download_url, {
363
- headers: { 'User-Agent': 'Haystack-CLI' },
364
- });
365
- if (!s3Resp.ok)
366
- return null;
367
- return await s3Resp.json();
368
- }
369
- return data;
370
- }
371
- catch (err) {
372
- if (err instanceof Error && err.name === 'HaystackApiError')
373
- throw err;
374
- // Transient network/parse failure — same "not available" semantics as 404.
375
- return null;
376
- }
377
- }
378
- /**
379
- * Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
380
- * a classified HaystackApiError — see fetchAutoFixManifest for why.
381
- */
382
- export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
383
- // Route through the auth-worker (/api/analysis/{prId}/rating_synthesis.json)
384
- // so hsk_live tokens are introspected and the scope gate applies — see
385
- // ANALYSIS_API_BASE comment above.
386
- const url = v3ResultUrl(owner, repo, prNumber, 'rating_synthesis.json');
387
- try {
388
- const response = await fetch(url, {
389
- headers: apiHeaders(token),
390
- });
391
- if (response.status === 401 || response.status === 403) {
392
- throw await classifyHttpError(response, 'Rating synthesis fetch');
393
- }
394
- if (!response.ok)
395
- return null;
396
- const data = await response.json();
397
- // Handle presigned URL redirect (Lambda returns { download_url } for large files)
398
- // Presigned S3 URLs include auth in query params — do NOT forward the GitHub token
399
- // (S3 rejects requests with both presigned auth and an Authorization/custom header)
400
- if (data.download_url) {
401
- const s3Resp = await fetch(data.download_url, {
402
- headers: { 'User-Agent': 'Haystack-CLI' },
403
- });
404
- if (!s3Resp.ok)
405
- return null;
406
- return await s3Resp.json();
407
- }
408
- return data;
409
- }
410
- catch (err) {
411
- if (err instanceof Error && err.name === 'HaystackApiError')
412
- throw err;
413
- // Transient network/parse failure — same "not available" semantics as 404.
414
- return null;
415
- }
416
- }