@haystackeditor/cli 0.15.21 → 0.15.22
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 +28 -2
- package/dist/commands/verify-core.d.ts +26 -1
- package/dist/commands/verify-core.js +59 -23
- package/dist/commands/verify-hosted.d.ts +60 -0
- package/dist/commands/verify-hosted.js +342 -0
- package/dist/commands/verify-mcp.js +59 -0
- package/dist/commands/verify-ops.js +1 -1
- package/dist/commands/verify-reproducibility.d.ts +85 -0
- package/dist/commands/verify-reproducibility.js +494 -0
- package/dist/commands/verify.d.ts +11 -0
- package/dist/commands/verify.js +55 -22
- package/dist/index.js +71 -0
- package/dist/schema.d.ts +1 -0
- package/dist/schema.js +1 -0
- package/dist/utils/haystack-api.d.ts +1 -1
- package/dist/utils/haystack-api.js +2 -2
- package/package.json +1 -1
- package/schemas/cloud-verifier.v1.json +96 -0
package/dist/commands/verify.js
CHANGED
|
@@ -4,7 +4,51 @@ import { resolve } from 'node:path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { setVerifyToken, verifyAuthHeaders } from './verify-core.js';
|
|
6
6
|
const DEFAULT_CONTROL_PLANE = 'http://127.0.0.1:3000';
|
|
7
|
-
const
|
|
7
|
+
export const VERIFY_SPECIMENS = new Set(['all', 'posthog-js-478', 'analytics-js-255']);
|
|
8
|
+
export function parseStartRunResponse(raw) {
|
|
9
|
+
let parsed;
|
|
10
|
+
try {
|
|
11
|
+
parsed = JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error('Verifier returned an invalid JSON response.');
|
|
15
|
+
}
|
|
16
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
17
|
+
throw new Error('Verifier returned an invalid response object.');
|
|
18
|
+
}
|
|
19
|
+
const response = parsed;
|
|
20
|
+
const requiredString = (field) => {
|
|
21
|
+
const value = response[field];
|
|
22
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
23
|
+
throw new Error(`Verifier response is missing a valid ${field}.`);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
runId: requiredString('runId'),
|
|
29
|
+
status: requiredString('status'),
|
|
30
|
+
resultsUrl: requiredString('resultsUrl'),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function verifierErrorReason(raw, contentType) {
|
|
34
|
+
if (!contentType?.toLowerCase().includes('application/json'))
|
|
35
|
+
return raw;
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
return `malformed JSON error response (${error instanceof Error ? error.message : String(error)}); raw body: ${raw}`;
|
|
42
|
+
}
|
|
43
|
+
if (parsed
|
|
44
|
+
&& typeof parsed === 'object'
|
|
45
|
+
&& !Array.isArray(parsed)
|
|
46
|
+
&& typeof parsed.error === 'string'
|
|
47
|
+
&& parsed.error.trim() !== '') {
|
|
48
|
+
return parsed.error;
|
|
49
|
+
}
|
|
50
|
+
return `JSON error response omitted a string error; raw body: ${raw}`;
|
|
51
|
+
}
|
|
8
52
|
function openResults(url) {
|
|
9
53
|
const platform = process.platform;
|
|
10
54
|
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
@@ -24,18 +68,15 @@ async function readIntentTranscript(path) {
|
|
|
24
68
|
throw new Error(`Could not read intent transcript at ${absolutePath}: ${message}`);
|
|
25
69
|
}
|
|
26
70
|
}
|
|
27
|
-
export async function
|
|
71
|
+
export async function startVerificationRun(specimenArg, options) {
|
|
28
72
|
const specimen = specimenArg || 'all';
|
|
29
|
-
if (!
|
|
73
|
+
if (!VERIFY_SPECIMENS.has(specimen)) {
|
|
30
74
|
throw new Error(`Unknown or unreproduced specimen "${specimen}". Choose all, posthog-js-478, or analytics-js-255.`);
|
|
31
75
|
}
|
|
32
76
|
setVerifyToken(options.token);
|
|
33
77
|
const server = (options.server || process.env.HAYSTACK_VERIFY_SERVER || DEFAULT_CONTROL_PLANE)
|
|
34
78
|
.replace(/\/+$/, '');
|
|
35
79
|
const intentTranscript = await readIntentTranscript(options.intentTranscript);
|
|
36
|
-
if (!options.json) {
|
|
37
|
-
console.error(chalk.dim('Preparing the cloud verification run…'));
|
|
38
|
-
}
|
|
39
80
|
let response;
|
|
40
81
|
try {
|
|
41
82
|
response = await fetch(`${server}/api/cloud-verifier/runs`, {
|
|
@@ -47,6 +88,7 @@ export async function verifyCommand(specimenArg, options) {
|
|
|
47
88
|
baseRef: options.base,
|
|
48
89
|
headRef: options.head,
|
|
49
90
|
intentTranscript,
|
|
91
|
+
planSourceRunId: options.planSourceRunId,
|
|
50
92
|
}),
|
|
51
93
|
});
|
|
52
94
|
}
|
|
@@ -59,25 +101,16 @@ export async function verifyCommand(specimenArg, options) {
|
|
|
59
101
|
throw new Error(`Control plane at ${server} requires a token (HTTP 401). Pass --token or set HAYSTACK_VERIFY_TOKEN.`);
|
|
60
102
|
}
|
|
61
103
|
if (!response.ok) {
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
reason = JSON.parse(raw).error || raw;
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// Preserve the response body when it is not JSON.
|
|
68
|
-
}
|
|
104
|
+
const reason = verifierErrorReason(raw, response.headers.get('content-type'));
|
|
69
105
|
throw new Error(`Verifier rejected the run (${response.status}): ${reason}`);
|
|
70
106
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
throw new Error('Verifier returned an invalid response.');
|
|
77
|
-
}
|
|
78
|
-
if (!started.runId || !started.resultsUrl) {
|
|
79
|
-
throw new Error('Verifier response is missing runId or resultsUrl.');
|
|
107
|
+
return parseStartRunResponse(raw);
|
|
108
|
+
}
|
|
109
|
+
export async function verifyCommand(specimenArg, options) {
|
|
110
|
+
if (!options.json) {
|
|
111
|
+
console.error(chalk.dim('Preparing the cloud verification run…'));
|
|
80
112
|
}
|
|
113
|
+
const started = await startVerificationRun(specimenArg, options);
|
|
81
114
|
if (options.json) {
|
|
82
115
|
process.stdout.write(`${JSON.stringify({
|
|
83
116
|
schema_version: '1.0.0',
|
package/dist/index.js
CHANGED
|
@@ -187,7 +187,9 @@ The local control plane must be running. From the Haystack repository:
|
|
|
187
187
|
pnpm dev
|
|
188
188
|
|
|
189
189
|
Examples:
|
|
190
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha>
|
|
190
191
|
haystack verify posthog-js-478 # start a run, then:
|
|
192
|
+
haystack verify reproducibility posthog-js-478 --runs 3
|
|
191
193
|
haystack verify watch # block until it finishes (exit 1 = tests failed)
|
|
192
194
|
haystack verify status # one-screen verdicts + live environment URLs
|
|
193
195
|
haystack verify findings # every problem found, with the readings that moved
|
|
@@ -203,6 +205,75 @@ Examples:
|
|
|
203
205
|
`)
|
|
204
206
|
.action((specimen, options) => runPublicCommand(() => verifyCommand(specimen, options), options.json));
|
|
205
207
|
const verifyRunArg = 'Run id, chapter id (newest run of that chapter), or "latest" (default)';
|
|
208
|
+
const hostedVerify = verify
|
|
209
|
+
.command('hosted')
|
|
210
|
+
.description('Run the production Cloud Verifier against exact GitHub commits');
|
|
211
|
+
hostedVerify
|
|
212
|
+
.command('start')
|
|
213
|
+
.description('Start an authenticated production verification and wait for its terminal result')
|
|
214
|
+
.argument('<repository>', 'Exact GitHub owner/repository name')
|
|
215
|
+
// `verify` already owns --base/--head for the local specimen launcher.
|
|
216
|
+
// Re-declaring these as Commander requiredOption values makes the parent
|
|
217
|
+
// consume them and the child then reject a valid invocation. Runtime
|
|
218
|
+
// validation below accepts the inherited values and still fails closed.
|
|
219
|
+
.option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
|
|
220
|
+
.option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
|
|
221
|
+
.option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
|
|
222
|
+
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
223
|
+
.option('--no-wait', 'Return after the production workflow is queued')
|
|
224
|
+
.option('--interval <seconds>', 'Polling interval while waiting (default 5)')
|
|
225
|
+
.option('--timeout <minutes>', 'Maximum wait (default 35)')
|
|
226
|
+
.option('--json', 'Versioned machine-readable run status and terminal output')
|
|
227
|
+
.addHelpText('after', `
|
|
228
|
+
The default idempotency key makes an exact retry return the original run. Pass
|
|
229
|
+
--idempotency-key with a new stable key only when you intentionally want a
|
|
230
|
+
fresh production execution of the same commit pair.
|
|
231
|
+
|
|
232
|
+
Examples:
|
|
233
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha>
|
|
234
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --no-wait --json
|
|
235
|
+
`)
|
|
236
|
+
.action(async (repository, _options, cmd) => {
|
|
237
|
+
const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
|
|
238
|
+
const { verifyHostedStartCommand } = await import('./commands/verify-hosted.js');
|
|
239
|
+
return runPublicCommand(() => verifyHostedStartCommand(repository, options), options.json);
|
|
240
|
+
});
|
|
241
|
+
hostedVerify
|
|
242
|
+
.command('status')
|
|
243
|
+
.description('Read an owned production verification, optionally waiting until terminal')
|
|
244
|
+
.argument('<run-id>', 'Hosted run id (cv_ followed by 48 lowercase hex characters)')
|
|
245
|
+
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
246
|
+
.option('--wait', 'Keep polling until the run reaches a terminal status')
|
|
247
|
+
.option('--interval <seconds>', 'Polling interval while waiting (default 5)')
|
|
248
|
+
.option('--timeout <minutes>', 'Maximum wait (default 35)')
|
|
249
|
+
.option('--json', 'Versioned machine-readable run status and terminal output')
|
|
250
|
+
.addHelpText('after', `
|
|
251
|
+
Example:
|
|
252
|
+
haystack verify hosted status cv_<48 lowercase hex characters> --wait --json
|
|
253
|
+
`)
|
|
254
|
+
.action(async (runId, _options, cmd) => {
|
|
255
|
+
const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
|
|
256
|
+
const { verifyHostedStatusCommand } = await import('./commands/verify-hosted.js');
|
|
257
|
+
return runPublicCommand(() => verifyHostedStatusCommand(runId, options), options.json);
|
|
258
|
+
});
|
|
259
|
+
verify
|
|
260
|
+
.command('reproducibility')
|
|
261
|
+
.description('Separate frozen-plan drift from executor/judgment nondeterminism')
|
|
262
|
+
.argument('<specimen>', 'Specimen id, e.g. posthog-js-478')
|
|
263
|
+
.option('--runs <n>', 'Number of sequential runs to compare (default 3; min 2, max 20)')
|
|
264
|
+
.option('--scope <scope>', 'executor replays one frozen plan (default); planner/full independently replan each run', 'executor')
|
|
265
|
+
.option('--plan-source <runId>', 'executor scope: replay this existing structured frozen plan for every run')
|
|
266
|
+
.option('--interval <seconds>', 'Polling interval for each run (default 3)')
|
|
267
|
+
.option('--timeout <minutes>', 'Per-run timeout (default: wait forever)')
|
|
268
|
+
.option('--server <url>', 'Verifier control-plane URL (default: http://127.0.0.1:3000)')
|
|
269
|
+
.option('--token <token>', 'Bearer token for a control plane that requires auth (or HAYSTACK_VERIFY_TOKEN)')
|
|
270
|
+
.option('--repo-root <path>', 'Repository holding .haystack/verify (default: walk up from cwd)')
|
|
271
|
+
.option('--json', 'Machine-readable stable/unstable cell report')
|
|
272
|
+
.action(async (specimen, commandOptions, cmd) => {
|
|
273
|
+
const options = { ...cmd.optsWithGlobals(), ...commandOptions };
|
|
274
|
+
const { verifyReproducibilityCommand } = await import('./commands/verify-reproducibility.js');
|
|
275
|
+
return runPublicCommand(() => verifyReproducibilityCommand(specimen, options), options.json);
|
|
276
|
+
});
|
|
206
277
|
verify
|
|
207
278
|
.command('status')
|
|
208
279
|
.description('One screen for a run: per-test verdicts, counts, live environments and expiry')
|
package/dist/schema.d.ts
CHANGED
package/dist/schema.js
CHANGED
|
@@ -27,7 +27,7 @@ export declare class HaystackApiError extends Error {
|
|
|
27
27
|
* class it failed with, and the next command to run.
|
|
28
28
|
*/
|
|
29
29
|
export declare function classifyHttpError(response: Response, context: string): Promise<HaystackApiError>;
|
|
30
|
-
export declare function haystackJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
|
|
30
|
+
export declare function haystackJson<T>(path: string, token: string, init?: RequestInit, acceptedStatuses?: readonly number[]): Promise<T>;
|
|
31
31
|
export declare function fetchCliIdentity(token: string): Promise<CliIdentity>;
|
|
32
32
|
export declare function fetchHaystackInstallations(token: string): Promise<UserInstallation[]>;
|
|
33
33
|
export declare function fetchGitHubJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
|
|
@@ -69,12 +69,12 @@ export async function classifyHttpError(response, context) {
|
|
|
69
69
|
}
|
|
70
70
|
return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
|
|
71
71
|
}
|
|
72
|
-
export async function haystackJson(path, token, init = {}) {
|
|
72
|
+
export async function haystackJson(path, token, init = {}, acceptedStatuses = []) {
|
|
73
73
|
const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
|
|
74
74
|
...init,
|
|
75
75
|
headers: headers(token, init.headers),
|
|
76
76
|
});
|
|
77
|
-
if (!response.ok) {
|
|
77
|
+
if (!response.ok && !acceptedStatuses.includes(response.status)) {
|
|
78
78
|
throw await classifyHttpError(response, `Haystack API ${path}`);
|
|
79
79
|
}
|
|
80
80
|
return response.json();
|
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://docs.haystackeditor.com/cli/schemas/cloud-verifier.v1.json",
|
|
4
|
+
"title": "Haystack hosted Cloud Verifier result",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"operation",
|
|
10
|
+
"run_id",
|
|
11
|
+
"status"
|
|
12
|
+
],
|
|
13
|
+
"properties": {
|
|
14
|
+
"schema_version": {
|
|
15
|
+
"const": "1.0.0"
|
|
16
|
+
},
|
|
17
|
+
"operation": {
|
|
18
|
+
"enum": [
|
|
19
|
+
"start",
|
|
20
|
+
"status"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"run_id": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"pattern": "^cv_[0-9a-f]{48}$"
|
|
26
|
+
},
|
|
27
|
+
"status": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"pattern": "^[a-z][a-z0-9_-]{1,31}$"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$"
|
|
34
|
+
},
|
|
35
|
+
"base_commit": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"pattern": "^[0-9a-f]{40}$"
|
|
38
|
+
},
|
|
39
|
+
"head_commit": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^[0-9a-f]{40}$"
|
|
42
|
+
},
|
|
43
|
+
"created_at": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"format": "date-time"
|
|
46
|
+
},
|
|
47
|
+
"completed_at": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"format": "date-time"
|
|
50
|
+
},
|
|
51
|
+
"account": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"pattern": "^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$"
|
|
54
|
+
},
|
|
55
|
+
"idempotency_key": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$"
|
|
58
|
+
},
|
|
59
|
+
"next_command": {
|
|
60
|
+
"type": "string"
|
|
61
|
+
},
|
|
62
|
+
"timed_out": {
|
|
63
|
+
"type": "boolean"
|
|
64
|
+
},
|
|
65
|
+
"output": {
|
|
66
|
+
"type": "object"
|
|
67
|
+
},
|
|
68
|
+
"error": {
|
|
69
|
+
"type": "string"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"allOf": [
|
|
73
|
+
{
|
|
74
|
+
"if": {
|
|
75
|
+
"properties": {
|
|
76
|
+
"status": {
|
|
77
|
+
"const": "complete"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"required": [
|
|
81
|
+
"status"
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
"then": {
|
|
85
|
+
"required": [
|
|
86
|
+
"repository",
|
|
87
|
+
"base_commit",
|
|
88
|
+
"head_commit",
|
|
89
|
+
"created_at",
|
|
90
|
+
"completed_at",
|
|
91
|
+
"output"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|