@haystackeditor/cli 0.25.1 → 0.27.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.
- package/README.md +39 -463
- package/dist/capture/app-config.js +30 -6
- package/dist/commands/capture-brief.js +45 -35
- package/dist/commands/capture-contract.js +4 -4
- package/dist/commands/crawl-report.js +196 -0
- package/dist/commands/db-profile-upload.js +3 -3
- package/dist/commands/feedback.js +66 -0
- package/dist/commands/init-telemetry.js +55 -9
- package/dist/commands/init.js +8 -3
- package/dist/commands/lockfile-pin.js +307 -0
- package/dist/commands/telemetry-token.js +3 -3
- package/dist/commands/tokens.js +4 -4
- package/dist/commands/verify-explore.js +3 -3
- package/dist/commands/verify-history.js +2 -2
- package/dist/commands/verify-onboarding.js +13 -16
- package/dist/commands/verify-precompute.js +3 -4
- package/dist/commands/verify.js +31 -118
- package/dist/index.js +55 -968
- package/dist/schema.js +4 -10
- package/dist/utils/haystack-api.js +8 -36
- package/package.json +1 -5
- package/schemas/feedback.v1.json +13 -0
- package/schemas/pre-verify.v2.json +240 -0
- package/schemas/verify-raw.v1.json +1132 -0
- package/schemas/verify.v2.json +655 -0
- package/dist/assets/hooks/agent-context/detect.ts +0 -316
- package/dist/assets/hooks/agent-context/format.ts +0 -100
- package/dist/assets/hooks/agent-context/index.ts +0 -41
- package/dist/assets/hooks/agent-context/parsers/claude.ts +0 -262
- package/dist/assets/hooks/agent-context/parsers/codex.ts +0 -416
- package/dist/assets/hooks/agent-context/parsers/gemini.ts +0 -155
- package/dist/assets/hooks/agent-context/parsers/opencode.ts +0 -174
- package/dist/assets/hooks/agent-context/tsconfig.json +0 -14
- package/dist/assets/hooks/agent-context/types.ts +0 -58
- package/dist/assets/hooks/llm-rules-template.md +0 -59
- package/dist/assets/hooks/package-lock.json +0 -598
- package/dist/assets/hooks/package.json +0 -12
- package/dist/assets/hooks/scripts/commit-msg.sh +0 -5
- package/dist/assets/hooks/scripts/post-commit.sh +0 -5
- package/dist/assets/hooks/scripts/pre-commit.sh +0 -175
- package/dist/assets/hooks/scripts/pre-push.sh +0 -25
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +0 -5
- package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +0 -528
- package/dist/assets/hooks/truncation-checker/index.ts +0 -595
- package/dist/assets/hooks/truncation-checker/tsconfig.json +0 -13
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +0 -2051
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +0 -4
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +0 -3411
- package/dist/assets/skills/map-your-system.md +0 -143
- package/dist/assets/skills/submit.md +0 -200
- package/dist/commands/ask.js +0 -20
- package/dist/commands/cloud-verifier-behaviors.js +0 -218
- package/dist/commands/cloud-verifier-data-store-census.js +0 -539
- package/dist/commands/cloud-verifier-data-store-drift.js +0 -158
- package/dist/commands/cloud-verifier-identity-census.js +0 -4060
- package/dist/commands/cloud-verifier-materialization.js +0 -704
- package/dist/commands/cloud-verifier-pascal-selector-census.js +0 -1382
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +0 -2015
- package/dist/commands/cloud-verifier-specialized-operational-census.js +0 -11432
- package/dist/commands/cloud-verifier-universe.js +0 -10178
- package/dist/commands/config.js +0 -549
- package/dist/commands/design-verify.js +0 -311
- package/dist/commands/dismiss.js +0 -159
- package/dist/commands/hooks.js +0 -226
- package/dist/commands/inbox.js +0 -137
- package/dist/commands/mcp.js +0 -201
- package/dist/commands/policy.js +0 -371
- package/dist/commands/pr-status.js +0 -207
- package/dist/commands/pr.js +0 -105
- package/dist/commands/prepare-universe-review.js +0 -1092
- package/dist/commands/production-source-deny-policy.js +0 -100
- package/dist/commands/request-review.js +0 -74
- package/dist/commands/review.js +0 -191
- package/dist/commands/rules.js +0 -98
- package/dist/commands/scaffold-provisional-universe.js +0 -806
- package/dist/commands/setup.js +0 -1170
- package/dist/commands/skills.js +0 -447
- package/dist/commands/status.js +0 -35
- package/dist/commands/submit.js +0 -745
- package/dist/commands/system-map.js +0 -228
- package/dist/commands/triage.js +0 -598
- package/dist/commands/webhooks.js +0 -241
- package/dist/states.js +0 -46
- package/dist/tools/detect.js +0 -832
- package/dist/triage/astra.js +0 -202
- package/dist/triage/prompts.js +0 -188
- package/dist/triage/runner.js +0 -200
- package/dist/triage/types.js +0 -7
- package/dist/types.js +0 -326
- package/dist/utils/action-output.js +0 -26
- package/dist/utils/analysis-api.js +0 -416
- package/dist/utils/config.js +0 -54
- package/dist/utils/design-verifier-api.js +0 -294
- package/dist/utils/design-verifier-history.js +0 -79
- package/dist/utils/design-verifier-result.js +0 -424
- package/dist/utils/github-api.js +0 -324
- package/dist/utils/pending-state.js +0 -86
- package/dist/utils/pr-ref.js +0 -56
- package/dist/utils/prompter.js +0 -328
- package/schemas/action.v1.json +0 -22
- package/schemas/ask.v1.json +0 -40
- package/schemas/inbox.v1.json +0 -27
- package/schemas/pr-status.v1.json +0 -61
- package/schemas/pr.v1.json +0 -97
- package/schemas/pr.v3.json +0 -45
- package/schemas/setup.v1.json +0 -75
- package/schemas/submit.v1.json +0 -90
- package/schemas/triage.v1.json +0 -103
- 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
|
-
}
|
package/dist/utils/config.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Configuration file utilities
|
|
3
|
-
*/
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
const CONFIG_FILENAME = '.haystack.json';
|
|
7
|
-
/**
|
|
8
|
-
* Find the config file by walking up the directory tree
|
|
9
|
-
*/
|
|
10
|
-
export async function findConfigPath(startDir = process.cwd()) {
|
|
11
|
-
let dir = startDir;
|
|
12
|
-
for (;;) {
|
|
13
|
-
const configPath = path.join(dir, CONFIG_FILENAME);
|
|
14
|
-
try {
|
|
15
|
-
await fs.access(configPath);
|
|
16
|
-
return configPath;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
// Not found, try parent
|
|
20
|
-
}
|
|
21
|
-
const parent = path.dirname(dir);
|
|
22
|
-
if (parent === dir) {
|
|
23
|
-
// Reached root
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
dir = parent;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Load and parse the config file
|
|
31
|
-
*/
|
|
32
|
-
export async function loadConfig(configPath) {
|
|
33
|
-
const resolvedPath = configPath || (await findConfigPath());
|
|
34
|
-
if (!resolvedPath) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
try {
|
|
38
|
-
const content = await fs.readFile(resolvedPath, 'utf-8');
|
|
39
|
-
return JSON.parse(content);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
throw new Error(`Failed to parse ${resolvedPath}: ${err.message}`);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Get the project root (directory containing .haystack.json or current dir)
|
|
47
|
-
*/
|
|
48
|
-
export async function getProjectRoot() {
|
|
49
|
-
const configPath = await findConfigPath();
|
|
50
|
-
if (configPath) {
|
|
51
|
-
return path.dirname(configPath);
|
|
52
|
-
}
|
|
53
|
-
return process.cwd();
|
|
54
|
-
}
|