@haystackeditor/cli 0.15.25 → 0.15.27

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 (51) hide show
  1. package/README.md +39 -3
  2. package/dist/assets/hooks/package-lock.json +598 -0
  3. package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +2051 -0
  4. package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +4 -0
  5. package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +3411 -0
  6. package/dist/assets/telemetry/browser-runtime.js +861 -0
  7. package/dist/assets/telemetry/runtime.cjs +1509 -0
  8. package/dist/commands/cloud-verifier-behaviors.d.ts +27 -0
  9. package/dist/commands/cloud-verifier-behaviors.js +218 -0
  10. package/dist/commands/cloud-verifier-data-store-census.d.ts +47 -0
  11. package/dist/commands/cloud-verifier-data-store-census.js +539 -0
  12. package/dist/commands/cloud-verifier-data-store-drift.d.ts +42 -0
  13. package/dist/commands/cloud-verifier-data-store-drift.js +158 -0
  14. package/dist/commands/cloud-verifier-identity-census.d.ts +88 -0
  15. package/dist/commands/cloud-verifier-identity-census.js +4057 -0
  16. package/dist/commands/cloud-verifier-materialization.d.ts +16 -0
  17. package/dist/commands/cloud-verifier-materialization.js +704 -0
  18. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +29 -0
  19. package/dist/commands/cloud-verifier-pascal-selector-census.js +1382 -0
  20. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +27 -0
  21. package/dist/commands/cloud-verifier-python-manifest-selector-census.js +2015 -0
  22. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +51 -0
  23. package/dist/commands/cloud-verifier-specialized-operational-census.js +11432 -0
  24. package/dist/commands/cloud-verifier-universe.d.ts +31 -0
  25. package/dist/commands/cloud-verifier-universe.js +10178 -0
  26. package/dist/commands/design-verify.d.ts +23 -0
  27. package/dist/commands/design-verify.js +142 -0
  28. package/dist/commands/hooks.js +2 -2
  29. package/dist/commands/prepare-universe-review.d.ts +115 -0
  30. package/dist/commands/prepare-universe-review.js +1092 -0
  31. package/dist/commands/production-source-deny-policy.d.ts +15 -0
  32. package/dist/commands/production-source-deny-policy.js +100 -0
  33. package/dist/commands/scaffold-provisional-universe.d.ts +468 -0
  34. package/dist/commands/scaffold-provisional-universe.js +808 -0
  35. package/dist/commands/schema-cmd.js +32 -0
  36. package/dist/commands/skills.d.ts +2 -2
  37. package/dist/commands/skills.js +415 -47
  38. package/dist/commands/telemetry.d.ts +53 -0
  39. package/dist/commands/telemetry.js +832 -0
  40. package/dist/commands/verify-hosted-reproducibility.d.ts +6 -2
  41. package/dist/commands/verify-hosted-reproducibility.js +65 -20
  42. package/dist/index.js +142 -13
  43. package/dist/triage/runner.js +7 -6
  44. package/dist/utils/design-verifier-api.d.ts +129 -0
  45. package/dist/utils/design-verifier-api.js +175 -0
  46. package/dist/utils/design-verifier-result.d.ts +52 -0
  47. package/dist/utils/design-verifier-result.js +299 -0
  48. package/dist/utils/haystack-api.d.ts +2 -0
  49. package/dist/utils/haystack-api.js +10 -3
  50. package/dist/utils/hooks.js +2 -1
  51. package/package.json +7 -2
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Client for the design-verifier trigger route on the auth worker.
3
+ *
4
+ * The worker owns everything sensitive: it authenticates the CLI token,
5
+ * enforces hsk_live repo scope, resolves the PR's base/head SHAs via the
6
+ * GitHub App install token, and forwards to the AWS design-verifier API with
7
+ * the server-side shared secret. This client just POSTs and interprets the
8
+ * response.
9
+ */
10
+ import { haystackApiUrl } from './haystack-api.js';
11
+ function isDuration(value) {
12
+ return value === null || (typeof value === 'number' && Number.isFinite(value) && value >= 0);
13
+ }
14
+ function isTimestamp(value) {
15
+ return value === null || (typeof value === 'string' && Number.isFinite(Date.parse(value)));
16
+ }
17
+ function isTimingSummary(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ return false;
20
+ const timing = value;
21
+ if (!isDuration(timing.admissionMs) || !isDuration(timing.pipelineDurationMs) ||
22
+ typeof timing.recordedStageMs !== 'number' || !isDuration(timing.recordedStageMs) ||
23
+ !isDuration(timing.unattributedMs) ||
24
+ !Array.isArray(timing.stages))
25
+ return false;
26
+ const expectedStages = ['prepare', 'bake', 'blast-radius', 'prefetch', 'case-design', 'verdict'];
27
+ return timing.stages.length === expectedStages.length && timing.stages.every((stage, index) => stage && typeof stage === 'object' && stage.stage === expectedStages[index] &&
28
+ typeof stage.status === 'string' && isDuration(stage.durationMs) && isTimestamp(stage.completedAt) &&
29
+ ['phase-artifact', 'phase-artifact-clock', 'cache-hit', 'missing'].includes(stage.source) &&
30
+ stage.breakdownMs && typeof stage.breakdownMs === 'object' && !Array.isArray(stage.breakdownMs) &&
31
+ Object.values(stage.breakdownMs).every((duration) => typeof duration === 'number' && isDuration(duration)));
32
+ }
33
+ function isDesignVerifierStatusResult(value, expected) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value))
35
+ return false;
36
+ const run = value;
37
+ if (run.version !== 'dv-cli-status-v1' || run.runId !== expected.runId ||
38
+ run.owner !== expected.owner || run.repo !== expected.repo || run.pr !== expected.pr ||
39
+ typeof run.status !== 'string')
40
+ return false;
41
+ if (run.status !== 'succeeded')
42
+ return true;
43
+ return !!run.result && typeof run.result === 'object' &&
44
+ run.result.verdict && typeof run.result.verdict === 'object' &&
45
+ Array.isArray(run.result.cases) && Array.isArray(run.result.scenarios) &&
46
+ Array.isArray(run.result.changedFiles) && Array.isArray(run.result.probes) &&
47
+ (run.result.timings === undefined || isTimingSummary(run.result.timings)) &&
48
+ run.result.replay && typeof run.result.replay === 'object';
49
+ }
50
+ function apiHeaders(token) {
51
+ return {
52
+ 'Cache-Control': 'no-cache',
53
+ 'User-Agent': 'Haystack-CLI',
54
+ Authorization: `Bearer ${token}`,
55
+ };
56
+ }
57
+ export async function triggerDesignVerifier(owner, repo, prNumber, token) {
58
+ const url = haystackApiUrl(`/api/design-verifier/trigger/${owner}/${repo}/${prNumber}`);
59
+ let response;
60
+ try {
61
+ response = await fetch(url, {
62
+ method: 'POST',
63
+ headers: { ...apiHeaders(token), 'Content-Type': 'application/json' },
64
+ body: '{}',
65
+ });
66
+ }
67
+ catch (err) {
68
+ return { status: 'error', message: err.message };
69
+ }
70
+ let data = {};
71
+ let parseFailed = false;
72
+ try {
73
+ data = await response.json();
74
+ }
75
+ catch {
76
+ // Recorded, not swallowed: non-OK statuses are classified below; a 2xx
77
+ // with an unparseable body becomes an explicit error rather than a
78
+ // silently-empty success.
79
+ parseFailed = true;
80
+ }
81
+ if (parseFailed && response.ok) {
82
+ return { status: 'error', message: `Trigger endpoint returned invalid JSON (HTTP ${response.status})` };
83
+ }
84
+ if (response.ok) {
85
+ const outcome = data.outcome === 'queued' || data.outcome === 'duplicate' ? data.outcome : 'started';
86
+ if (!data.runId) {
87
+ return { status: 'error', message: `Trigger succeeded but no runId was returned (HTTP ${response.status})` };
88
+ }
89
+ return { status: outcome, runId: data.runId };
90
+ }
91
+ const message = data.message || data.error || `HTTP ${response.status}`;
92
+ // API Gateway answers 403 "Missing Authentication Token" for routes that
93
+ // do not exist — which is exactly what the flag-gated backend looks like
94
+ // before enable_design_verifier is on. The worker's own denials carry
95
+ // specific error codes/messages, so this signature is unambiguous.
96
+ if (response.status === 403 && data.message === 'Missing Authentication Token') {
97
+ return { status: 'unavailable', message: 'Design verifier is not enabled in this environment yet.' };
98
+ }
99
+ if (response.status === 401 || response.status === 403)
100
+ return { status: 'denied', message };
101
+ if (response.status === 429)
102
+ return { status: 'rate_limited', message };
103
+ if (response.status === 404) {
104
+ // Two distinct 404s share this status: the worker's own pr_not_found
105
+ // (carries an error code and a specific message) versus the AWS gateway
106
+ // 404 for the flag-gated /v3/design-verifier route not being enabled
107
+ // yet. Only the latter means "feature unavailable".
108
+ if (data.error === 'pr_not_found') {
109
+ return { status: 'error', message };
110
+ }
111
+ return { status: 'unavailable', message: 'Design verifier is not enabled in this environment yet.' };
112
+ }
113
+ return { status: 'error', message };
114
+ }
115
+ export async function getDesignVerifierStatus(owner, repo, prNumber, runId, token) {
116
+ const url = haystackApiUrl(`/api/design-verifier/status/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${prNumber}/${encodeURIComponent(runId)}`);
117
+ let response;
118
+ try {
119
+ response = await fetch(url, { method: 'GET', headers: apiHeaders(token) });
120
+ }
121
+ catch (err) {
122
+ return { status: 'error', message: err.message };
123
+ }
124
+ let data = {};
125
+ try {
126
+ data = await response.json();
127
+ }
128
+ catch {
129
+ return { status: 'error', message: `Status endpoint returned invalid JSON (HTTP ${response.status})` };
130
+ }
131
+ if (response.ok) {
132
+ if (!isDesignVerifierStatusResult(data, { owner, repo, pr: prNumber, runId })) {
133
+ return { status: 'error', message: `Status endpoint returned an invalid or mismatched result (HTTP ${response.status})` };
134
+ }
135
+ return { status: 'ok', run: data };
136
+ }
137
+ const message = data.message || data.error || `HTTP ${response.status}`;
138
+ if (response.status === 401 || response.status === 403)
139
+ return { status: 'denied', message };
140
+ if (response.status === 404)
141
+ return { status: 'not_found', message };
142
+ if (response.status === 502 || response.status === 503)
143
+ return { status: 'unavailable', message };
144
+ return { status: 'error', message };
145
+ }
146
+ export async function waitForDesignVerifier(owner, repo, prNumber, runId, token, options = {}) {
147
+ const intervalMs = options.intervalMs ?? 3000;
148
+ const timeoutMs = options.timeoutMs ?? 2 * 60 * 60 * 1000;
149
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
150
+ const deadline = Date.now() + timeoutMs;
151
+ let consecutiveErrors = 0;
152
+ let lastStage;
153
+ for (;;) {
154
+ const loaded = await getDesignVerifierStatus(owner, repo, prNumber, runId, token);
155
+ if (loaded.status !== 'ok') {
156
+ consecutiveErrors += 1;
157
+ if (loaded.status === 'denied' || loaded.status === 'not_found' || consecutiveErrors >= 3) {
158
+ return { status: 'error', message: loaded.message };
159
+ }
160
+ }
161
+ else {
162
+ consecutiveErrors = 0;
163
+ if (loaded.run.stage !== lastStage) {
164
+ lastStage = loaded.run.stage;
165
+ options.onProgress?.(loaded.run);
166
+ }
167
+ if (['succeeded', 'failed', 'failed-stale'].includes(loaded.run.status)) {
168
+ return { status: 'terminal', run: loaded.run };
169
+ }
170
+ }
171
+ if (Date.now() >= deadline)
172
+ return { status: 'timeout' };
173
+ await sleep(intervalMs);
174
+ }
175
+ }
@@ -0,0 +1,52 @@
1
+ import type { DesignVerifierCommandSpec, DesignVerifierExecutionSummary, DesignVerifierStatusResult, DesignVerifierTimingSummary } from './design-verifier-api.js';
2
+ export type DesignVerifierAgentVerdict = 'fix_confirmed' | 'fix_needed' | 'no_unexpected_change' | 'inconclusive';
3
+ export interface DesignVerifierAgentCase {
4
+ id: string;
5
+ title: string;
6
+ story: string;
7
+ expectedHead: string;
8
+ verdict: 'confirmed' | 'finding' | 'clean' | 'inconclusive';
9
+ findingKind: string | null;
10
+ baseStable: boolean | null;
11
+ observations: Array<{
12
+ path: string;
13
+ base: unknown;
14
+ baseRepeat: unknown;
15
+ head: unknown;
16
+ }>;
17
+ generatedFiles: string[];
18
+ executedCommand: DesignVerifierCommandSpec | null;
19
+ executions: DesignVerifierExecutionSummary[];
20
+ infrastructure: {
21
+ verified: boolean;
22
+ recipeId: string | null;
23
+ freshInstances: number;
24
+ baseRepeat: string | null;
25
+ errors: string[];
26
+ } | null;
27
+ }
28
+ export interface DesignVerifierAgentReport {
29
+ ok: boolean;
30
+ status: 'complete' | 'failed';
31
+ verdict: DesignVerifierAgentVerdict;
32
+ headline: string;
33
+ explanation: string;
34
+ runId: string;
35
+ repository: string;
36
+ pullRequest: number;
37
+ revisions: {
38
+ base: string | null;
39
+ head: string | null;
40
+ };
41
+ durationMs: number | null;
42
+ timings: DesignVerifierTimingSummary | null;
43
+ changedFiles: string[];
44
+ cases: DesignVerifierAgentCase[];
45
+ inspectCommand: string;
46
+ replay: Record<string, unknown> | null;
47
+ error: string | null;
48
+ exitCode: 0 | 1 | 2;
49
+ }
50
+ export declare function buildDesignVerifierAgentReport(run: DesignVerifierStatusResult): DesignVerifierAgentReport;
51
+ export declare function formatDesignVerifierAgentReport(report: DesignVerifierAgentReport): string;
52
+ export declare function designVerifierStageLabel(stage: string | null): string;
@@ -0,0 +1,299 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ function findingExplanation(kind) {
3
+ if (kind === 'declared-change-not-observed') {
4
+ return 'The intended behavior change was not observed on the proposed revision.';
5
+ }
6
+ if (kind === 'unexpected-change') {
7
+ return 'The proposed revision changed behavior that was registered to remain stable.';
8
+ }
9
+ return 'Executed behavior contradicted the registered expectation.';
10
+ }
11
+ function sameRecordedValue(left, right) {
12
+ return isDeepStrictEqual(left, right);
13
+ }
14
+ function executionSummaries(value) {
15
+ if (!Array.isArray(value))
16
+ return [];
17
+ return value.filter((execution) => !!execution && typeof execution === 'object' &&
18
+ ['base1', 'base2', 'head'].includes(execution.side) &&
19
+ ['completed', 'failed', 'missing'].includes(execution.status) &&
20
+ Array.isArray(execution.diagnostics) && Array.isArray(execution.outputExcerpt));
21
+ }
22
+ export function buildDesignVerifierAgentReport(run) {
23
+ const repository = `${run.owner}/${run.repo}`;
24
+ if (run.status !== 'succeeded' || !run.result) {
25
+ return {
26
+ ok: false,
27
+ status: 'failed',
28
+ verdict: 'inconclusive',
29
+ headline: 'VERIFICATION INCONCLUSIVE',
30
+ explanation: run.error || 'The verifier did not produce a terminal behavioral verdict.',
31
+ runId: run.runId,
32
+ repository,
33
+ pullRequest: run.pr,
34
+ revisions: { base: run.baseSha, head: run.headSha },
35
+ durationMs: run.durationMs,
36
+ timings: null,
37
+ changedFiles: [],
38
+ cases: [],
39
+ inspectCommand: `haystack design-verify ${repository}#${run.pr} --run ${run.runId}`,
40
+ replay: null,
41
+ error: run.error,
42
+ exitCode: 2,
43
+ };
44
+ }
45
+ const verdict = run.result.verdict || {};
46
+ const summary = verdict.summary || {};
47
+ const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
48
+ const confirmations = Array.isArray(verdict.confirmations) ? verdict.confirmations : [];
49
+ const findingByCase = new Map(findings.map((finding) => [finding.caseId, finding]));
50
+ const confirmedIds = new Set(confirmations.map((confirmation) => confirmation.caseId));
51
+ const scenarioByCase = new Map(run.result.scenarios.map((scenario) => [scenario.caseId, scenario]));
52
+ const probeByCase = new Map(run.result.probes.map((probe) => [probe.caseId, probe]));
53
+ const cases = run.result.cases.map((testCase) => {
54
+ const id = String(testCase.id || '');
55
+ const finding = findingByCase.get(id);
56
+ const scenario = scenarioByCase.get(id);
57
+ const probe = probeByCase.get(id);
58
+ const observations = (Array.isArray(scenario?.divergences) ? scenario.divergences : []).map((item) => ({
59
+ path: String(item.display || (Array.isArray(item.path) ? item.path.join('.') : 'observation')),
60
+ base: item.base,
61
+ baseRepeat: item.base2,
62
+ head: item.head,
63
+ }));
64
+ return {
65
+ id,
66
+ title: String(testCase.title || scenario?.scenario || id),
67
+ story: String(testCase.story || ''),
68
+ expectedHead: String(testCase.expected_head_observation || ''),
69
+ verdict: finding
70
+ ? 'finding'
71
+ : confirmedIds.has(id)
72
+ ? 'confirmed'
73
+ : scenario?.status === 'clean'
74
+ ? 'clean'
75
+ : 'inconclusive',
76
+ findingKind: finding?.kind || null,
77
+ baseStable: observations.length
78
+ ? observations.every((item) => sameRecordedValue(item.base, item.baseRepeat))
79
+ : scenario?.status === 'clean' ? true : null,
80
+ observations,
81
+ generatedFiles: probe?.files || [],
82
+ executedCommand: probe?.run || null,
83
+ executions: executionSummaries(scenario?.executions),
84
+ infrastructure: scenario?.infrastructureReceipt &&
85
+ typeof scenario.infrastructureReceipt === 'object'
86
+ ? {
87
+ verified: scenario.infrastructureReceipt.status === 'passed',
88
+ recipeId: typeof scenario.infrastructureReceipt.recipeId === 'string'
89
+ ? scenario.infrastructureReceipt.recipeId
90
+ : null,
91
+ freshInstances: Array.isArray(scenario.infrastructureReceipt.environmentInstanceIds)
92
+ ? scenario.infrastructureReceipt.environmentInstanceIds.length
93
+ : 0,
94
+ baseRepeat: typeof scenario.infrastructureReceipt.baseRepeat === 'string'
95
+ ? scenario.infrastructureReceipt.baseRepeat
96
+ : null,
97
+ errors: Array.isArray(scenario.infrastructureReceipt.errors)
98
+ ? scenario.infrastructureReceipt.errors.filter((item) => typeof item === 'string')
99
+ : [],
100
+ }
101
+ : null,
102
+ };
103
+ });
104
+ let agentVerdict;
105
+ let headline;
106
+ let explanation;
107
+ let exitCode;
108
+ if (findings.length > 0) {
109
+ agentVerdict = 'fix_needed';
110
+ headline = 'FIX NEEDED';
111
+ explanation = findingExplanation(String(findings[0]?.kind || ''));
112
+ exitCode = 1;
113
+ }
114
+ else if (summary.caseDesignOutcome === 'no-supported-cases' || Number(summary.scenarios || 0) === 0) {
115
+ agentVerdict = 'inconclusive';
116
+ headline = 'VERIFICATION INCONCLUSIVE';
117
+ explanation = 'The verifier could not design and execute a supported behavioral case.';
118
+ exitCode = 2;
119
+ }
120
+ else if (confirmations.length > 0) {
121
+ agentVerdict = 'fix_confirmed';
122
+ headline = 'FIX CONFIRMED';
123
+ explanation = 'The intended behavior change was observed on head and reproduced across two stable base runs.';
124
+ exitCode = 0;
125
+ }
126
+ else {
127
+ agentVerdict = 'no_unexpected_change';
128
+ headline = 'NO UNEXPECTED CHANGE';
129
+ explanation = 'Executed cases found no behavior change outside the registered expectations.';
130
+ exitCode = 0;
131
+ }
132
+ return {
133
+ ok: exitCode === 0,
134
+ status: 'complete',
135
+ verdict: agentVerdict,
136
+ headline,
137
+ explanation,
138
+ runId: run.runId,
139
+ repository,
140
+ pullRequest: run.pr,
141
+ revisions: { base: run.baseSha, head: run.headSha },
142
+ durationMs: run.durationMs,
143
+ timings: run.result.timings || null,
144
+ changedFiles: run.result.changedFiles,
145
+ cases,
146
+ inspectCommand: `haystack design-verify ${repository}#${run.pr} --run ${run.runId}`,
147
+ replay: run.result.replay,
148
+ error: null,
149
+ exitCode,
150
+ };
151
+ }
152
+ function displayValue(value) {
153
+ const encoded = JSON.stringify(value);
154
+ return encoded === undefined ? String(value) : encoded;
155
+ }
156
+ function durationLabel(durationMs) {
157
+ if (durationMs === null)
158
+ return '';
159
+ const totalSeconds = Math.round(durationMs / 1000);
160
+ const minutes = Math.floor(totalSeconds / 60);
161
+ const seconds = totalSeconds % 60;
162
+ return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
163
+ }
164
+ function preciseDurationLabel(durationMs) {
165
+ if (durationMs === null)
166
+ return 'not recorded';
167
+ if (durationMs < 1000)
168
+ return `${Number(durationMs.toFixed(3))}ms`;
169
+ const minutes = Math.floor(durationMs / 60_000);
170
+ const seconds = Number(((durationMs % 60_000) / 1000).toFixed(3));
171
+ return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
172
+ }
173
+ function timingStageLabel(stage) {
174
+ if (stage === 'blast-radius')
175
+ return 'blast radius';
176
+ if (stage === 'case-design')
177
+ return 'case design';
178
+ return stage;
179
+ }
180
+ function commandLabel(command) {
181
+ if (!command)
182
+ return null;
183
+ return command.argv.map((arg) => (/^[A-Za-z0-9_./:=+-]+$/.test(arg) ? arg : JSON.stringify(arg))).join(' ');
184
+ }
185
+ function executionSideLabel(side) {
186
+ if (side === 'base1')
187
+ return 'base';
188
+ if (side === 'base2')
189
+ return 'base repeat';
190
+ return 'head';
191
+ }
192
+ function executionNeedsAttention(execution) {
193
+ return execution.status !== 'completed' || execution.diagnostics.length > 0;
194
+ }
195
+ export function formatDesignVerifierAgentReport(report) {
196
+ const timing = durationLabel(report.durationMs);
197
+ const lines = [
198
+ `${report.exitCode === 0 ? '✓' : report.exitCode === 1 ? '✗' : '?'} ${report.headline}${timing ? ` ${timing}` : ''}`,
199
+ '',
200
+ report.explanation,
201
+ ];
202
+ if (report.revisions.base || report.revisions.head) {
203
+ lines.push('', `Revisions: base ${report.revisions.base || 'unknown'} (executed twice)`, ` head ${report.revisions.head || 'unknown'} (executed once)`);
204
+ }
205
+ if (report.timings) {
206
+ lines.push('', 'Timing breakdown:');
207
+ if (report.timings.admissionMs !== null) {
208
+ lines.push(` admission: ${preciseDurationLabel(report.timings.admissionMs)}`);
209
+ }
210
+ for (const stage of report.timings.stages) {
211
+ lines.push(` ${timingStageLabel(stage.stage)}: ${preciseDurationLabel(stage.durationMs)} (${stage.status})`);
212
+ }
213
+ lines.push(` pipeline total: ${preciseDurationLabel(report.timings.pipelineDurationMs)}`);
214
+ if (report.timings.unattributedMs !== null) {
215
+ lines.push(` orchestration/finalization not recorded in a phase: ${preciseDurationLabel(report.timings.unattributedMs)}`);
216
+ }
217
+ }
218
+ for (const testCase of report.cases) {
219
+ lines.push('', `Test: ${testCase.title}`);
220
+ if (testCase.story)
221
+ lines.push(` Behavior: ${testCase.story}`);
222
+ if (testCase.expectedHead)
223
+ lines.push(` Expected on head: ${testCase.expectedHead}`);
224
+ if (testCase.observations.length) {
225
+ lines.push(' Observed:');
226
+ for (const observation of testCase.observations) {
227
+ lines.push(` ${observation.path}`);
228
+ lines.push(` base: ${displayValue(observation.base)} (repeat: ${displayValue(observation.baseRepeat)})`);
229
+ lines.push(` head: ${displayValue(observation.head)}`);
230
+ }
231
+ }
232
+ if (testCase.baseStable !== null) {
233
+ lines.push(` Base stability: ${testCase.baseStable ? 'passed; both runs agreed' : 'failed; base runs disagreed'}`);
234
+ }
235
+ if (testCase.infrastructure) {
236
+ lines.push(testCase.infrastructure.verified
237
+ ? ` Infrastructure: verified; same recipe, ${testCase.infrastructure.freshInstances} fresh instances`
238
+ : ` Infrastructure: unverified; ${testCase.infrastructure.errors.join('; ') || 'receipt missing'}`);
239
+ }
240
+ const executionsWithDiagnostics = testCase.executions.filter(executionNeedsAttention);
241
+ if (executionsWithDiagnostics.length) {
242
+ lines.push(' Execution diagnostics:');
243
+ for (const execution of executionsWithDiagnostics) {
244
+ const exit = execution.commandExitCode === null ? 'exit unknown' : `exit ${execution.commandExitCode}`;
245
+ const elapsed = execution.durationMs === null ? '' : `, ${durationLabel(execution.durationMs)}`;
246
+ lines.push(` ${executionSideLabel(execution.side)} (${exit}${elapsed}):`);
247
+ if (execution.scenarioError)
248
+ lines.push(` Scenario error: ${execution.scenarioError}`);
249
+ for (const diagnostic of execution.diagnostics) {
250
+ const identity = [diagnostic.type, diagnostic.code].filter(Boolean).join(', ');
251
+ lines.push(` ${diagnostic.kind}${identity ? ` (${identity})` : ''}: ${diagnostic.message}`);
252
+ }
253
+ if (execution.diagnosticsOmitted > 0) {
254
+ lines.push(` ... ${execution.diagnosticsOmitted} additional diagnostics omitted`);
255
+ }
256
+ if (execution.outputExcerpt.length) {
257
+ lines.push(' Relevant command output:');
258
+ for (const output of execution.outputExcerpt) {
259
+ lines.push(` ${output.stream}: ${output.text}`);
260
+ }
261
+ if (execution.outputLinesOmitted > 0) {
262
+ lines.push(` ... ${execution.outputLinesOmitted} output lines omitted`);
263
+ }
264
+ }
265
+ lines.push(` Full output artifact: ${execution.outputArtifact}`);
266
+ lines.push(` Structured trace artifact: ${execution.traceArtifact}`);
267
+ }
268
+ }
269
+ if (testCase.generatedFiles.length)
270
+ lines.push(` Generated probe artifact: ${testCase.generatedFiles.join(', ')}`);
271
+ const command = commandLabel(testCase.executedCommand);
272
+ if (command)
273
+ lines.push(` Executed in the sandbox: ${command}`);
274
+ }
275
+ if (report.changedFiles.length) {
276
+ lines.push('', 'Changed files to inspect:', ...report.changedFiles.map((file) => ` ${file}`));
277
+ }
278
+ lines.push('', `Inspect this exact result again: ${report.inspectCommand}`, `Run: ${report.runId}`);
279
+ return lines.join('\n');
280
+ }
281
+ export function designVerifierStageLabel(stage) {
282
+ if (!stage)
283
+ return 'Waiting for the run to start';
284
+ if (stage === 'prepare' || stage === 'bake')
285
+ return 'Preparing the repository';
286
+ if (stage === 'blast-radius')
287
+ return 'Mapping affected behavior';
288
+ if (stage === 'prefetch')
289
+ return 'Retrieving relevant failure patterns';
290
+ if (stage === 'stage1')
291
+ return 'Designing behavioral test cases';
292
+ if (stage.startsWith('build:'))
293
+ return 'Building an executable probe';
294
+ if (stage.startsWith('execute:'))
295
+ return 'Running base/base/head comparisons';
296
+ if (stage === 'verdict')
297
+ return 'Comparing observations';
298
+ return stage;
299
+ }
@@ -1,3 +1,5 @@
1
+ export declare function haystackApiBase(): string;
2
+ export declare function haystackApiUrl(path: string): string;
1
3
  export interface UserInstallation {
2
4
  id: number;
3
5
  account?: {
@@ -1,4 +1,11 @@
1
- const HAYSTACK_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
1
+ export function haystackApiBase() {
2
+ return process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
3
+ }
4
+ export function haystackApiUrl(path) {
5
+ if (!path.startsWith('/'))
6
+ throw new Error('Haystack API path must be absolute');
7
+ return `${haystackApiBase()}${path}`;
8
+ }
2
9
  /**
3
10
  * Typed error for non-OK Haystack API responses. Callers classify by
4
11
  * `status` (never by message substring) and the message always carries a
@@ -70,7 +77,7 @@ export async function classifyHttpError(response, context) {
70
77
  return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
71
78
  }
72
79
  export async function haystackJson(path, token, init = {}, acceptedStatuses = []) {
73
- const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
80
+ const response = await fetch(haystackApiUrl(path), {
74
81
  ...init,
75
82
  headers: headers(token, init.headers),
76
83
  });
@@ -91,7 +98,7 @@ export async function fetchGitHubJson(path, token, init = {}) {
91
98
  }
92
99
  export async function fetchAnalysisArtifact(owner, repo, prNumber, file, token) {
93
100
  const ref = encodeURIComponent(`${owner}/${repo}#${prNumber}`);
94
- const response = await fetch(`${HAYSTACK_API_BASE}/api/analysis/${ref}/${file}`, {
101
+ const response = await fetch(haystackApiUrl(`/api/analysis/${ref}/${file}`), {
95
102
  headers: headers(token, { 'Cache-Control': 'no-cache' }),
96
103
  });
97
104
  if (response.status === 404)
@@ -169,10 +169,11 @@ export async function copyHookFiles(hooksDir) {
169
169
  await copyDirRecursive(path.join(assetsDir, 'truncation-checker'), path.join(hooksDir, 'truncation-checker'));
170
170
  // Copy hooks package.json
171
171
  await fs.copyFile(path.join(assetsDir, 'package.json'), path.join(hooksDir, 'package.json'));
172
+ await fs.copyFile(path.join(assetsDir, 'package-lock.json'), path.join(hooksDir, 'package-lock.json'));
172
173
  }
173
174
  /** Install npm dependencies in the hooks directory */
174
175
  export async function installHookDeps(hooksDir) {
175
- execSync('npm install --no-fund --no-audit', {
176
+ execSync('npm ci --ignore-scripts --no-fund --no-audit', {
176
177
  cwd: hooksDir,
177
178
  stdio: 'pipe',
178
179
  timeout: 60_000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.25",
3
+ "version": "0.15.27",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,10 +10,13 @@
10
10
  "build": "rm -rf dist && tsc && cp -r src/assets dist/assets",
11
11
  "dev": "tsc --watch",
12
12
  "start": "node dist/index.js",
13
+ "benchmark:telemetry": "pnpm build && node scripts/benchmark-telemetry-overhead.mjs",
14
+ "benchmark:telemetry:corpus": "pnpm build && node scripts/benchmark-open-source-telemetry.mjs",
13
15
  "check:schemas": "node scripts/check-schemas.mjs",
14
16
  "release": "node scripts/release.mjs",
15
17
  "prepublishOnly": "node scripts/no-direct-publish.mjs",
16
- "test": "pnpm build && vitest run && node --test tests/*.test.mjs"
18
+ "test": "pnpm build && vitest run && node --test tests/*.test.mjs",
19
+ "test:package": "node scripts/package-smoke.mjs"
17
20
  },
18
21
  "keywords": [
19
22
  "haystack",
@@ -35,6 +38,7 @@
35
38
  "url": "https://github.com/haystackeditor/haystack-review/issues"
36
39
  },
37
40
  "dependencies": {
41
+ "@babel/core": "7.29.0",
38
42
  "@modelcontextprotocol/sdk": "1.26.0",
39
43
  "chalk": "5.6.2",
40
44
  "commander": "12.1.0",
@@ -56,6 +60,7 @@
56
60
  "dist",
57
61
  "schemas"
58
62
  ],
63
+ "bundledDependencies": [],
59
64
  "engines": {
60
65
  "node": ">=18"
61
66
  }