@haystackeditor/cli 0.15.28 → 0.15.29
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/dist/assets/telemetry/browser-runtime.js +19 -2
- package/dist/commands/verify-hosted-mcp.js +12 -2
- package/dist/commands/verify-hosted-reproducibility.d.ts +1 -0
- package/dist/commands/verify-hosted-reproducibility.js +4 -1
- package/dist/commands/verify-hosted.d.ts +7 -0
- package/dist/commands/verify-hosted.js +70 -10
- package/dist/commands/verify-precompute.js +2 -2
- package/dist/index.js +8 -6
- package/package.json +1 -1
|
@@ -868,7 +868,7 @@ var __haystackWebAtlasModuleApi = (function installHaystackWebAtlas(host) {
|
|
|
868
868
|
return result;
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
-
function axiosAt(token, owner, callable, receiver, configIndex, safeConfig, applicationArguments) {
|
|
871
|
+
function axiosAt(token, owner, callable, receiver, ownerProperty, configIndex, safeConfig, applicationArguments) {
|
|
872
872
|
if (!nativeArrayIsArray(applicationArguments) || typeof callable !== 'function') {
|
|
873
873
|
return nativeReflectApply(callable, receiver, applicationArguments);
|
|
874
874
|
}
|
|
@@ -877,7 +877,10 @@ var __haystackWebAtlasModuleApi = (function installHaystackWebAtlas(host) {
|
|
|
877
877
|
if (context && !admitProbe(context)) return nativeReflectApply(callable, receiver, applicationArguments);
|
|
878
878
|
var sites = context && context.runtimeConfig && context.runtimeConfig.fetchSites[token];
|
|
879
879
|
if (!context || !sites) return nativeReflectApply(callable, receiver, applicationArguments);
|
|
880
|
-
if (
|
|
880
|
+
if ((ownerProperty !== null && (typeof ownerProperty !== 'string' || ownerProperty.length === 0 ||
|
|
881
|
+
ownerProperty.length > 256 || ownerProperty === '__proto__' || ownerProperty === 'constructor' ||
|
|
882
|
+
ownerProperty === 'prototype')) || safeConfig !== true || !Number.isInteger(configIndex) ||
|
|
883
|
+
configIndex < 0 || configIndex > 8 ||
|
|
881
884
|
!platformFetchCorrelationAllowed()) {
|
|
882
885
|
omit(context);
|
|
883
886
|
return nativeReflectApply(callable, receiver, applicationArguments);
|
|
@@ -886,6 +889,20 @@ var __haystackWebAtlasModuleApi = (function installHaystackWebAtlas(host) {
|
|
|
886
889
|
var args = [];
|
|
887
890
|
var config;
|
|
888
891
|
try {
|
|
892
|
+
if (ownerProperty !== null) {
|
|
893
|
+
if ((typeof receiver !== 'object' || receiver === null) && typeof receiver !== 'function') {
|
|
894
|
+
axiosOmission(state);
|
|
895
|
+
return nativeReflectApply(callable, receiver, applicationArguments);
|
|
896
|
+
}
|
|
897
|
+
var ownerDescriptor = nativeGetOwnPropertyDescriptor(receiver, ownerProperty);
|
|
898
|
+
if (!ownerDescriptor || !nativeReflectApply(nativeHasOwnProperty, ownerDescriptor, ['value']) ||
|
|
899
|
+
((typeof ownerDescriptor.value !== 'object' || ownerDescriptor.value === null) &&
|
|
900
|
+
typeof ownerDescriptor.value !== 'function')) {
|
|
901
|
+
axiosOmission(state);
|
|
902
|
+
return nativeReflectApply(callable, receiver, applicationArguments);
|
|
903
|
+
}
|
|
904
|
+
owner = ownerDescriptor.value;
|
|
905
|
+
}
|
|
889
906
|
for (var argumentIndex = 0; argumentIndex < applicationArguments.length; argumentIndex++) {
|
|
890
907
|
args[argumentIndex] = applicationArguments[argumentIndex];
|
|
891
908
|
}
|
|
@@ -7,7 +7,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
7
7
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
8
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
9
9
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
10
|
-
import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
10
|
+
import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
11
11
|
const TERMINAL_STATUSES = new Set([
|
|
12
12
|
'complete',
|
|
13
13
|
'errored',
|
|
@@ -48,6 +48,11 @@ export const HOSTED_VERIFY_TOOLS = [
|
|
|
48
48
|
pattern: '^[0-9a-f]{40}$',
|
|
49
49
|
description: 'Exact 40-character lowercase head commit SHA.',
|
|
50
50
|
},
|
|
51
|
+
intent_file: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
minLength: 1,
|
|
54
|
+
description: 'Path to agent-supplied JSON containing exactly problem, goal, and intended_outcomes (a non-empty array of non-empty strings).',
|
|
55
|
+
},
|
|
51
56
|
idempotency_key: {
|
|
52
57
|
type: 'string',
|
|
53
58
|
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
|
|
@@ -59,7 +64,7 @@ export const HOSTED_VERIFY_TOOLS = [
|
|
|
59
64
|
},
|
|
60
65
|
account: accountInput,
|
|
61
66
|
},
|
|
62
|
-
required: ['repository', 'base_commit', 'head_commit'],
|
|
67
|
+
required: ['repository', 'base_commit', 'head_commit', 'intent_file'],
|
|
63
68
|
},
|
|
64
69
|
},
|
|
65
70
|
{
|
|
@@ -124,6 +129,7 @@ function assertToolInputFields(name, input) {
|
|
|
124
129
|
'base_commit',
|
|
125
130
|
'head_commit',
|
|
126
131
|
'idempotency_key',
|
|
132
|
+
'intent_file',
|
|
127
133
|
'replay_plan_from_run_id',
|
|
128
134
|
'repository',
|
|
129
135
|
]);
|
|
@@ -151,6 +157,7 @@ export async function executeHostedVerifyTool(name, input, context) {
|
|
|
151
157
|
const started = await startHostedVerification(repository, {
|
|
152
158
|
base: requireInputString(input, 'base_commit'),
|
|
153
159
|
head: requireInputString(input, 'head_commit'),
|
|
160
|
+
intentFile: requireInputString(input, 'intent_file'),
|
|
154
161
|
idempotencyKey: optionalInputString(input, 'idempotency_key'),
|
|
155
162
|
replayPlanFrom: optionalInputString(input, 'replay_plan_from_run_id'),
|
|
156
163
|
wait: false,
|
|
@@ -217,6 +224,9 @@ export async function runHostedVerifyMcpServer(options) {
|
|
|
217
224
|
// Reject undeclared fields before account resolution so callers cannot
|
|
218
225
|
// use an invalid request to trigger credential or repository lookup.
|
|
219
226
|
assertToolInputFields(name, input);
|
|
227
|
+
if (name === 'verify_start') {
|
|
228
|
+
readHostedIntentFile(requireInputString(input, 'intent_file'));
|
|
229
|
+
}
|
|
220
230
|
const context = await resolveToolContext(name, input, options);
|
|
221
231
|
const result = await executeHostedVerifyTool(name, input, context);
|
|
222
232
|
return {
|
|
@@ -2,7 +2,7 @@ import { createHash, randomBytes } from 'node:crypto';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { withSchema } from '../schema.js';
|
|
4
4
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
5
|
-
import { hostedPollingOptions, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
5
|
+
import { hostedPollingOptions, parseHostedRepository, readHostedIntentFile, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
|
|
6
6
|
const CELL_ID = /^cell_[0-9a-f]{32}$/;
|
|
7
7
|
const CAPABILITY_ID = /^repository_driver_[0-9a-f]{64}$/;
|
|
8
8
|
const COMPARISON_ID = /^driver_comparison_[0-9a-f]{64}$/;
|
|
@@ -502,6 +502,8 @@ function renderHostedReproducibility(report) {
|
|
|
502
502
|
}
|
|
503
503
|
export async function verifyHostedReproducibilityCommand(repositoryArg, options) {
|
|
504
504
|
const repository = parseHostedRepository(repositoryArg);
|
|
505
|
+
// A reproducibility series reuses one explicit agent intent for every run.
|
|
506
|
+
readHostedIntentFile(options.intentFile);
|
|
505
507
|
const requestedRuns = boundedInteger(options.runs, DEFAULT_RUNS, '--runs', 2, MAX_RUNS);
|
|
506
508
|
const seriesKey = requireSeriesKey(options.seriesKey);
|
|
507
509
|
const mode = options.freshPlans
|
|
@@ -521,6 +523,7 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
|
|
|
521
523
|
const started = await startHostedVerification(repositoryArg, {
|
|
522
524
|
base: options.base,
|
|
523
525
|
head: options.head,
|
|
526
|
+
intentFile: options.intentFile,
|
|
524
527
|
idempotencyKey: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1, mode),
|
|
525
528
|
replayPlanFrom: options.freshPlans || index === 0
|
|
526
529
|
? undefined
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
export interface HostedIntent {
|
|
2
|
+
problem: string;
|
|
3
|
+
goal: string;
|
|
4
|
+
intended_outcomes: string[];
|
|
5
|
+
}
|
|
1
6
|
export interface HostedStartOptions {
|
|
2
7
|
base: string;
|
|
3
8
|
head: string;
|
|
9
|
+
intentFile: string;
|
|
4
10
|
idempotencyKey?: string;
|
|
5
11
|
replayPlanFrom?: string;
|
|
6
12
|
account?: string;
|
|
@@ -55,6 +61,7 @@ export declare function parseHostedRepository(value: string): {
|
|
|
55
61
|
repository: string;
|
|
56
62
|
fullName: string;
|
|
57
63
|
};
|
|
64
|
+
export declare function readHostedIntentFile(intentFile: string | undefined): HostedIntent;
|
|
58
65
|
export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string, replayPlanFrom?: string): string;
|
|
59
66
|
export declare function hostedPollingOptions(interval: string | undefined, timeout: string | undefined, onStatus?: (state: HostedRunState) => void): HostedPollOptions;
|
|
60
67
|
export declare function startHostedVerification(repositoryArg: string, options: HostedStartOptions, token: string): Promise<{
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { withSchema } from '../schema.js';
|
|
4
5
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
@@ -11,6 +12,11 @@ const STATUS = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
|
11
12
|
const TERMINAL_STATUSES = new Set(['complete', 'errored', 'expired', 'terminated']);
|
|
12
13
|
const DEFAULT_INTERVAL_SECONDS = 5;
|
|
13
14
|
const DEFAULT_TIMEOUT_MINUTES = 35;
|
|
15
|
+
const INTENT_FIELDS = [
|
|
16
|
+
'problem',
|
|
17
|
+
'goal',
|
|
18
|
+
'intended_outcomes',
|
|
19
|
+
];
|
|
14
20
|
function requireRecord(value, context) {
|
|
15
21
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
16
22
|
throw new Error(`${context} returned an invalid response object.`);
|
|
@@ -159,6 +165,54 @@ function requireCommit(value, flag) {
|
|
|
159
165
|
}
|
|
160
166
|
return value;
|
|
161
167
|
}
|
|
168
|
+
export function readHostedIntentFile(intentFile) {
|
|
169
|
+
if (typeof intentFile !== 'string' || intentFile.length === 0) {
|
|
170
|
+
throw new Error('--intent-file is required and must name a JSON file containing exactly problem, goal, and intended_outcomes.');
|
|
171
|
+
}
|
|
172
|
+
let source;
|
|
173
|
+
try {
|
|
174
|
+
source = readFileSync(intentFile, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
178
|
+
throw new Error(`Could not read --intent-file "${intentFile}": ${reason}`);
|
|
179
|
+
}
|
|
180
|
+
let parsed;
|
|
181
|
+
try {
|
|
182
|
+
parsed = JSON.parse(source);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
throw new Error(`--intent-file "${intentFile}" must contain valid JSON.`);
|
|
186
|
+
}
|
|
187
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
188
|
+
throw new Error(`--intent-file "${intentFile}" must contain exactly problem, goal, and intended_outcomes.`);
|
|
189
|
+
}
|
|
190
|
+
const record = parsed;
|
|
191
|
+
const unknownField = Object.keys(record)
|
|
192
|
+
.find(field => !INTENT_FIELDS.includes(field));
|
|
193
|
+
if (unknownField) {
|
|
194
|
+
throw new Error(`--intent-file "${intentFile}" contains unknown field "${unknownField}"; expected exactly problem, goal, and intended_outcomes.`);
|
|
195
|
+
}
|
|
196
|
+
for (const field of ['problem', 'goal']) {
|
|
197
|
+
if (typeof record[field] !== 'string' || record[field].trim().length === 0) {
|
|
198
|
+
throw new Error(`--intent-file "${intentFile}" field "${field}" must be a non-empty string.`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const outcomes = record.intended_outcomes;
|
|
202
|
+
if (!Array.isArray(outcomes)
|
|
203
|
+
|| outcomes.length === 0
|
|
204
|
+
|| outcomes.some(item => typeof item !== 'string' || item.trim().length === 0)) {
|
|
205
|
+
throw new Error(`--intent-file "${intentFile}" field "intended_outcomes" must be a non-empty array of non-empty strings.`);
|
|
206
|
+
}
|
|
207
|
+
if (Object.keys(record).length !== INTENT_FIELDS.length) {
|
|
208
|
+
throw new Error(`--intent-file "${intentFile}" must contain exactly problem, goal, and intended_outcomes.`);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
problem: record.problem,
|
|
212
|
+
goal: record.goal,
|
|
213
|
+
intended_outcomes: record.intended_outcomes,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
162
216
|
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom) {
|
|
163
217
|
const digest = createHash('sha256')
|
|
164
218
|
.update(JSON.stringify(replayPlanFrom
|
|
@@ -193,25 +247,28 @@ export async function startHostedVerification(repositoryArg, options, token) {
|
|
|
193
247
|
const repository = parseHostedRepository(repositoryArg);
|
|
194
248
|
const baseCommit = requireCommit(options.base, '--base');
|
|
195
249
|
const headCommit = requireCommit(options.head, '--head');
|
|
250
|
+
const intent = readHostedIntentFile(options.intentFile);
|
|
196
251
|
const replayPlanFrom = options.replayPlanFrom;
|
|
197
252
|
if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
|
|
198
253
|
throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
|
|
199
254
|
}
|
|
200
255
|
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
201
256
|
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom));
|
|
257
|
+
const launchBody = {
|
|
258
|
+
owner: repository.owner,
|
|
259
|
+
repository: repository.repository,
|
|
260
|
+
baseCommit,
|
|
261
|
+
headCommit,
|
|
262
|
+
idempotencyKey,
|
|
263
|
+
intent,
|
|
264
|
+
...(replayPlanFrom
|
|
265
|
+
? { replayPlanFromRunId: replayPlanFrom }
|
|
266
|
+
: {}),
|
|
267
|
+
};
|
|
202
268
|
const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
|
|
203
269
|
method: 'POST',
|
|
204
270
|
headers: { 'Content-Type': 'application/json' },
|
|
205
|
-
body: JSON.stringify(
|
|
206
|
-
owner: repository.owner,
|
|
207
|
-
repository: repository.repository,
|
|
208
|
-
baseCommit,
|
|
209
|
-
headCommit,
|
|
210
|
-
idempotencyKey,
|
|
211
|
-
...(replayPlanFrom
|
|
212
|
-
? { replayPlanFromRunId: replayPlanFrom }
|
|
213
|
-
: {}),
|
|
214
|
-
}),
|
|
271
|
+
body: JSON.stringify(launchBody),
|
|
215
272
|
});
|
|
216
273
|
const state = parseHostedRunState(response);
|
|
217
274
|
return {
|
|
@@ -337,6 +394,9 @@ function markNonSuccess(state, timedOut = false) {
|
|
|
337
394
|
}
|
|
338
395
|
export async function verifyHostedStartCommand(repositoryArg, options) {
|
|
339
396
|
const repository = parseHostedRepository(repositoryArg);
|
|
397
|
+
// Fail before account resolution so an invalid local intent never advances
|
|
398
|
+
// toward a hosted launch.
|
|
399
|
+
readHostedIntentFile(options.intentFile);
|
|
340
400
|
const auth = await resolveAuthContext({
|
|
341
401
|
preferredLogin: options.account,
|
|
342
402
|
owner: repository.owner,
|
|
@@ -140,9 +140,9 @@ function deriveWorkCommit(gitRoot, baseSha, treeSha, deadline) {
|
|
|
140
140
|
return workCommit;
|
|
141
141
|
}
|
|
142
142
|
function slimInputHash() {
|
|
143
|
-
// Mirrors printf '%s\0%s\0%s\0%s\0' slim
|
|
143
|
+
// Mirrors printf '%s\0%s\0%s\0%s\0' slim agent-deferred none none.
|
|
144
144
|
return createHash('sha256')
|
|
145
|
-
.update(Buffer.from('slim\
|
|
145
|
+
.update(Buffer.from('slim\0agent-deferred\0none\0none\0'))
|
|
146
146
|
.digest('hex');
|
|
147
147
|
}
|
|
148
148
|
async function buildCompressedPatch(gitRoot, baseSha, workCommit, deadline) {
|
package/dist/index.js
CHANGED
|
@@ -192,7 +192,7 @@ The local control plane must be running. From the Haystack repository:
|
|
|
192
192
|
pnpm dev
|
|
193
193
|
|
|
194
194
|
Examples:
|
|
195
|
-
haystack verify hosted start owner/repo --base <sha> --head <sha>
|
|
195
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
|
|
196
196
|
haystack verify posthog-js-478 # start a run, then:
|
|
197
197
|
haystack verify reproducibility posthog-js-478 --runs 3
|
|
198
198
|
haystack verify watch # block until it finishes (exit 1 = tests failed)
|
|
@@ -235,6 +235,7 @@ hostedVerify
|
|
|
235
235
|
// validation below accepts the inherited values and still fails closed.
|
|
236
236
|
.option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
|
|
237
237
|
.option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
|
|
238
|
+
.requiredOption('--intent-file <path>', 'Agent-supplied JSON with exactly problem, goal, and intended_outcomes')
|
|
238
239
|
.option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
|
|
239
240
|
.option('--replay-plan-from <run-id>', 'Reuse the exact validated risk plan from a completed owned run')
|
|
240
241
|
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
@@ -250,9 +251,9 @@ fresh production execution of the same commit pair.
|
|
|
250
251
|
a schema-9 terminal receipt with replay provenance.
|
|
251
252
|
|
|
252
253
|
Examples:
|
|
253
|
-
haystack verify hosted start owner/repo --base <sha> --head <sha>
|
|
254
|
-
haystack verify hosted start owner/repo --base <sha> --head <sha> --replay-plan-from cv_<48 lowercase hex characters>
|
|
255
|
-
haystack verify hosted start owner/repo --base <sha> --head <sha> --no-wait --json
|
|
254
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path>
|
|
255
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --replay-plan-from cv_<48 lowercase hex characters>
|
|
256
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --intent-file <path> --no-wait --json
|
|
256
257
|
`)
|
|
257
258
|
.action(async (repository, _options, cmd) => {
|
|
258
259
|
const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
|
|
@@ -283,6 +284,7 @@ hostedVerify
|
|
|
283
284
|
.argument('<repository>', 'Exact GitHub owner/repository name')
|
|
284
285
|
.option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
|
|
285
286
|
.option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
|
|
287
|
+
.requiredOption('--intent-file <path>', 'Agent-supplied JSON reused for every hosted run in the series')
|
|
286
288
|
.option('--runs <n>', 'Number of sequential production runs (default 3; min 2, max 10)')
|
|
287
289
|
.option('--series-key <key>', 'Stable series key for resumable idempotent retries')
|
|
288
290
|
.option('--fresh-plans', 'Run the planner independently for every run instead of replaying the first plan')
|
|
@@ -302,8 +304,8 @@ provenance, and no inconclusive cells. Use this mode to detect planner drift;
|
|
|
302
304
|
use the default mode to isolate executor or assessment drift.
|
|
303
305
|
|
|
304
306
|
Example:
|
|
305
|
-
haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3
|
|
306
|
-
haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3 --fresh-plans
|
|
307
|
+
haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --intent-file <path> --runs 3
|
|
308
|
+
haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --intent-file <path> --runs 3 --fresh-plans
|
|
307
309
|
`)
|
|
308
310
|
.action(async (repository, _options, cmd) => {
|
|
309
311
|
const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
|