@haystackeditor/cli 0.15.24 → 0.15.25
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 +23 -5
- package/dist/commands/verify-hosted-mcp.js +6 -0
- package/dist/commands/verify-hosted-reproducibility.d.ts +18 -6
- package/dist/commands/verify-hosted-reproducibility.js +113 -9
- package/dist/commands/verify-hosted.d.ts +2 -1
- package/dist/commands/verify-hosted.js +13 -4
- package/dist/index.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -105,6 +105,21 @@ haystack verify hosted status cv_<48-lowercase-hex-characters> \
|
|
|
105
105
|
--json
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
+
To execute a completed run's exact validated risk plan again, keep the same
|
|
109
|
+
repository and commit pair and pass:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
haystack verify hosted start owner/repo \
|
|
113
|
+
--base <40-character-commit-sha> \
|
|
114
|
+
--head <40-character-commit-sha> \
|
|
115
|
+
--replay-plan-from cv_<48-lowercase-hex-characters> \
|
|
116
|
+
--json
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A successful replay emits a schema-9 terminal result whose
|
|
120
|
+
`riskPlanReplayReceipt` binds the source run and canonical risk-plan hash. The
|
|
121
|
+
replay does not mint or deliver a planning-model credential.
|
|
122
|
+
|
|
108
123
|
Agents can use the same production lifecycle over stdio MCP:
|
|
109
124
|
|
|
110
125
|
```bash
|
|
@@ -116,7 +131,8 @@ The hosted server exposes only `verify_start`, `verify_status`, and
|
|
|
116
131
|
credentials, repository source, provider metadata, and sandbox mutation are
|
|
117
132
|
not exposed.
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
Freeze one production plan and repeat its exact execution to detect executor or
|
|
135
|
+
assessment drift:
|
|
120
136
|
|
|
121
137
|
```bash
|
|
122
138
|
haystack verify hosted reproducibility owner/repo \
|
|
@@ -126,10 +142,12 @@ haystack verify hosted reproducibility owner/repo \
|
|
|
126
142
|
--json
|
|
127
143
|
```
|
|
128
144
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
145
|
+
The first run creates the source plan; every later run names it through
|
|
146
|
+
`replayPlanFromRunId`. Each run gets a distinct idempotency key derived from one
|
|
147
|
+
printed series key. Pass `--series-key <key>` to resume the same series without
|
|
148
|
+
creating duplicate runs. The report verifies schema-9 replay provenance and
|
|
149
|
+
compares stable risk-cell, capability, and comparison IDs. Missing replay
|
|
150
|
+
provenance, assessment drift, timeouts, unsupported receipts, and inconclusive
|
|
133
151
|
cells all prevent a reproducible verdict and exit 2.
|
|
134
152
|
|
|
135
153
|
**`haystack setup --json`** speaks NDJSON: events (`question`, `permission`,
|
|
@@ -53,6 +53,10 @@ export const HOSTED_VERIFY_TOOLS = [
|
|
|
53
53
|
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
|
|
54
54
|
description: 'Optional stable retry key. Omit to derive it from the exact repository and commits.',
|
|
55
55
|
},
|
|
56
|
+
replay_plan_from_run_id: {
|
|
57
|
+
...runIdInput,
|
|
58
|
+
description: 'Optional completed owned run whose exact validated risk plan must be replayed.',
|
|
59
|
+
},
|
|
56
60
|
account: accountInput,
|
|
57
61
|
},
|
|
58
62
|
required: ['repository', 'base_commit', 'head_commit'],
|
|
@@ -120,6 +124,7 @@ function assertToolInputFields(name, input) {
|
|
|
120
124
|
'base_commit',
|
|
121
125
|
'head_commit',
|
|
122
126
|
'idempotency_key',
|
|
127
|
+
'replay_plan_from_run_id',
|
|
123
128
|
'repository',
|
|
124
129
|
]);
|
|
125
130
|
return;
|
|
@@ -147,6 +152,7 @@ export async function executeHostedVerifyTool(name, input, context) {
|
|
|
147
152
|
base: requireInputString(input, 'base_commit'),
|
|
148
153
|
head: requireInputString(input, 'head_commit'),
|
|
149
154
|
idempotencyKey: optionalInputString(input, 'idempotency_key'),
|
|
155
|
+
replayPlanFrom: optionalInputString(input, 'replay_plan_from_run_id'),
|
|
150
156
|
wait: false,
|
|
151
157
|
}, context.token);
|
|
152
158
|
return buildHostedJson('start', started.state, {
|
|
@@ -13,7 +13,18 @@ interface HostedReproducibilityRun {
|
|
|
13
13
|
state: HostedRunState;
|
|
14
14
|
timedOut: boolean;
|
|
15
15
|
}
|
|
16
|
+
interface HostedReproducibilityIdentity {
|
|
17
|
+
seriesKey: string;
|
|
18
|
+
repository: string;
|
|
19
|
+
baseCommit: string;
|
|
20
|
+
headCommit: string;
|
|
21
|
+
requestedRuns?: number;
|
|
22
|
+
}
|
|
16
23
|
interface ParsedAssessment {
|
|
24
|
+
schemaVersion: 8 | 9;
|
|
25
|
+
riskPlanSha256: string;
|
|
26
|
+
replaySourceRunId: string | null;
|
|
27
|
+
replaySourceResultSchemaVersion: 8 | 9 | null;
|
|
17
28
|
planIdentities: Array<{
|
|
18
29
|
cellId: string;
|
|
19
30
|
capabilityId: string | null;
|
|
@@ -50,8 +61,13 @@ export interface HostedReproducibilityReport {
|
|
|
50
61
|
base_commit: string;
|
|
51
62
|
head_commit: string;
|
|
52
63
|
requested_runs: number;
|
|
64
|
+
launched_runs: number;
|
|
65
|
+
unstarted_runs: number;
|
|
53
66
|
analyzable_runs: number;
|
|
54
67
|
run_ids: string[];
|
|
68
|
+
replay_source_run_id: string | null;
|
|
69
|
+
plan_replay_verified: boolean;
|
|
70
|
+
planner_reproducibility_tested: false;
|
|
55
71
|
planner_reproducible: boolean;
|
|
56
72
|
assessment_reproducible: boolean;
|
|
57
73
|
reproducible: boolean;
|
|
@@ -63,11 +79,7 @@ export interface HostedReproducibilityReport {
|
|
|
63
79
|
unrunnable_runs: RunFailure[];
|
|
64
80
|
}
|
|
65
81
|
export declare function hostedReproducibilityRunKey(seriesKey: string, repository: string, baseCommit: string, headCommit: string, ordinal: number): string;
|
|
66
|
-
export declare function analyzeHostedReproducibility(runs: HostedReproducibilityRun[], identity:
|
|
67
|
-
|
|
68
|
-
repository: string;
|
|
69
|
-
baseCommit: string;
|
|
70
|
-
headCommit: string;
|
|
71
|
-
}): HostedReproducibilityReport;
|
|
82
|
+
export declare function analyzeHostedReproducibility(runs: HostedReproducibilityRun[], identity: HostedReproducibilityIdentity): HostedReproducibilityReport;
|
|
83
|
+
export declare function isHostedReproducibilityReplaySource(run: HostedReproducibilityRun, identity: HostedReproducibilityIdentity): boolean;
|
|
72
84
|
export declare function verifyHostedReproducibilityCommand(repositoryArg: string, options: HostedReproducibilityOptions): Promise<void>;
|
|
73
85
|
export {};
|
|
@@ -6,6 +6,7 @@ import { hostedPollingOptions, parseHostedRepository, startHostedVerification, w
|
|
|
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}$/;
|
|
9
|
+
const RUN_ID = /^cv_[0-9a-f]{48}$/;
|
|
9
10
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
10
11
|
const SERIES_KEY = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,63}$/;
|
|
11
12
|
const TERMINAL_STATUSES = new Set([
|
|
@@ -80,9 +81,20 @@ export function hostedReproducibilityRunKey(seriesKey, repository, baseCommit, h
|
|
|
80
81
|
.digest('hex');
|
|
81
82
|
return `repro:${digest}`;
|
|
82
83
|
}
|
|
84
|
+
function stable(value) {
|
|
85
|
+
if (Array.isArray(value))
|
|
86
|
+
return value.map(stable);
|
|
87
|
+
if (value && typeof value === 'object') {
|
|
88
|
+
const object = value;
|
|
89
|
+
return Object.fromEntries(Object.keys(object)
|
|
90
|
+
.sort()
|
|
91
|
+
.map(key => [key, stable(object[key])]));
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
83
95
|
function fingerprint(value) {
|
|
84
96
|
return createHash('sha256')
|
|
85
|
-
.update(JSON.stringify(value))
|
|
97
|
+
.update(JSON.stringify(stable(value)))
|
|
86
98
|
.digest('hex');
|
|
87
99
|
}
|
|
88
100
|
function expectedCellStatus(reasonCode) {
|
|
@@ -124,11 +136,51 @@ function parseAssessment(state, identity) {
|
|
|
124
136
|
throw new Error('run identity does not match the requested exact launch');
|
|
125
137
|
}
|
|
126
138
|
const output = record(state.output, 'terminal output');
|
|
127
|
-
if (output.schemaVersion !== 8
|
|
139
|
+
if ((output.schemaVersion !== 8 && output.schemaVersion !== 9)
|
|
128
140
|
|| output.status !== 'driver_assessment_recorded') {
|
|
129
|
-
throw new Error('run did not produce a schema-8 driver assessment');
|
|
141
|
+
throw new Error('run did not produce a schema-8 or schema-9 driver assessment');
|
|
130
142
|
}
|
|
131
143
|
const riskPlan = record(output.riskPlanReceipt, 'risk plan receipt');
|
|
144
|
+
const riskPlanSha256 = fingerprint(riskPlan);
|
|
145
|
+
let replaySourceRunId = null;
|
|
146
|
+
let replaySourceResultSchemaVersion = null;
|
|
147
|
+
if (output.schemaVersion === 9) {
|
|
148
|
+
if (output.planningCredentialDelivery !== 'not-required-plan-replay') {
|
|
149
|
+
throw new Error('schema-9 replay used an invalid planning credential mode');
|
|
150
|
+
}
|
|
151
|
+
const replay = record(output.riskPlanReplayReceipt, 'risk plan replay receipt');
|
|
152
|
+
const fields = Object.keys(replay);
|
|
153
|
+
if (fields.length !== 6
|
|
154
|
+
|| fields.some(field => ![
|
|
155
|
+
'schemaVersion',
|
|
156
|
+
'sourceContentPersisted',
|
|
157
|
+
'sourceResultSchemaVersion',
|
|
158
|
+
'sourceRiskPlanReceiptSha256',
|
|
159
|
+
'sourceRunId',
|
|
160
|
+
'status',
|
|
161
|
+
].includes(field))
|
|
162
|
+
|| replay.schemaVersion !== 1
|
|
163
|
+
|| replay.status !== 'risk_plan_replay_verified'
|
|
164
|
+
|| typeof replay.sourceRunId !== 'string'
|
|
165
|
+
|| !RUN_ID.test(replay.sourceRunId)
|
|
166
|
+
|| (replay.sourceResultSchemaVersion !== 8
|
|
167
|
+
&& replay.sourceResultSchemaVersion !== 9)
|
|
168
|
+
|| replay.sourceRiskPlanReceiptSha256 !== riskPlanSha256
|
|
169
|
+
|| typeof replay.sourceRiskPlanReceiptSha256 !== 'string'
|
|
170
|
+
|| !SHA256.test(replay.sourceRiskPlanReceiptSha256)
|
|
171
|
+
|| replay.sourceContentPersisted !== false) {
|
|
172
|
+
throw new Error('risk plan replay receipt is malformed');
|
|
173
|
+
}
|
|
174
|
+
replaySourceRunId = replay.sourceRunId;
|
|
175
|
+
replaySourceResultSchemaVersion = replay.sourceResultSchemaVersion;
|
|
176
|
+
}
|
|
177
|
+
else if (output.riskPlanReplayReceipt !== undefined) {
|
|
178
|
+
throw new Error('schema-8 result contains unversioned replay provenance');
|
|
179
|
+
}
|
|
180
|
+
else if (output.planningCredentialDelivery
|
|
181
|
+
!== 'per-command-env-gateway-placeholder') {
|
|
182
|
+
throw new Error('schema-8 result used an invalid planning credential mode');
|
|
183
|
+
}
|
|
132
184
|
const assessment = record(output.driverAssessmentReceipt, 'driver assessment receipt');
|
|
133
185
|
const planCells = array(riskPlan.cells, 'risk plan cells');
|
|
134
186
|
const assessmentCells = array(assessment.cells, 'assessment cells');
|
|
@@ -219,6 +271,10 @@ function parseAssessment(state, identity) {
|
|
|
219
271
|
throw new Error('driver assessment overall status is inconsistent');
|
|
220
272
|
}
|
|
221
273
|
return {
|
|
274
|
+
schemaVersion: output.schemaVersion,
|
|
275
|
+
riskPlanSha256,
|
|
276
|
+
replaySourceRunId,
|
|
277
|
+
replaySourceResultSchemaVersion,
|
|
222
278
|
planIdentities,
|
|
223
279
|
cellIds,
|
|
224
280
|
status: assessment.status,
|
|
@@ -317,8 +373,18 @@ export function analyzeHostedReproducibility(runs, identity) {
|
|
|
317
373
|
}
|
|
318
374
|
const planVariants = groupedPlanVariants(parsed);
|
|
319
375
|
const assessmentVariants = groupedOutcomeVariants(parsed);
|
|
320
|
-
const
|
|
321
|
-
const
|
|
376
|
+
const replaySourceRunId = parsed[0]?.runId ?? null;
|
|
377
|
+
const planReplayVerified = parsed.length === runs.length
|
|
378
|
+
&& parsed.length > 1
|
|
379
|
+
&& parsed[0]?.assessment.schemaVersion === 8
|
|
380
|
+
&& parsed[0]?.assessment.replaySourceRunId === null
|
|
381
|
+
&& parsed.slice(1).every(item => item.assessment.schemaVersion === 9
|
|
382
|
+
&& item.assessment.replaySourceRunId === replaySourceRunId
|
|
383
|
+
&& item.assessment.replaySourceResultSchemaVersion
|
|
384
|
+
=== parsed[0].assessment.schemaVersion
|
|
385
|
+
&& item.assessment.riskPlanSha256
|
|
386
|
+
=== parsed[0].assessment.riskPlanSha256);
|
|
387
|
+
const assessmentReproducible = planReplayVerified && assessmentVariants.length === 1;
|
|
322
388
|
const planDriftCellIds = driftIds(parsed, assessment => new Map(assessment.planIdentities.map(plan => [
|
|
323
389
|
plan.cellId,
|
|
324
390
|
{ capabilityId: plan.capabilityId },
|
|
@@ -335,17 +401,25 @@ export function analyzeHostedReproducibility(runs, identity) {
|
|
|
335
401
|
const inconclusiveCellIds = [...new Set(parsed.flatMap(item => item.assessment.outcomes
|
|
336
402
|
.filter(outcome => outcome.status === 'inconclusive')
|
|
337
403
|
.map(outcome => outcome.cellId)))].sort();
|
|
404
|
+
const requestedRuns = identity.requestedRuns ?? runs.length;
|
|
338
405
|
return {
|
|
339
406
|
series_key: identity.seriesKey,
|
|
340
407
|
repository: identity.repository,
|
|
341
408
|
base_commit: identity.baseCommit,
|
|
342
409
|
head_commit: identity.headCommit,
|
|
343
|
-
requested_runs:
|
|
410
|
+
requested_runs: requestedRuns,
|
|
411
|
+
launched_runs: runs.length,
|
|
412
|
+
unstarted_runs: Math.max(0, requestedRuns - runs.length),
|
|
344
413
|
analyzable_runs: parsed.length,
|
|
345
414
|
run_ids: runs.map(run => run.state.runId),
|
|
346
|
-
|
|
415
|
+
replay_source_run_id: replaySourceRunId,
|
|
416
|
+
plan_replay_verified: planReplayVerified,
|
|
417
|
+
planner_reproducibility_tested: false,
|
|
418
|
+
planner_reproducible: false,
|
|
347
419
|
assessment_reproducible: assessmentReproducible,
|
|
348
|
-
reproducible:
|
|
420
|
+
reproducible: planReplayVerified
|
|
421
|
+
&& assessmentReproducible
|
|
422
|
+
&& inconclusiveCellIds.length === 0,
|
|
349
423
|
plan_variants: planVariants,
|
|
350
424
|
assessment_variants: assessmentVariants,
|
|
351
425
|
plan_drift_cell_ids: planDriftCellIds,
|
|
@@ -354,11 +428,25 @@ export function analyzeHostedReproducibility(runs, identity) {
|
|
|
354
428
|
unrunnable_runs: unrunnableRuns,
|
|
355
429
|
};
|
|
356
430
|
}
|
|
431
|
+
export function isHostedReproducibilityReplaySource(run, identity) {
|
|
432
|
+
if (run.timedOut || run.state.status !== 'complete')
|
|
433
|
+
return false;
|
|
434
|
+
try {
|
|
435
|
+
const assessment = parseAssessment(run.state, identity);
|
|
436
|
+
return assessment.schemaVersion === 8
|
|
437
|
+
&& assessment.replaySourceRunId === null;
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
357
443
|
function renderHostedReproducibility(report) {
|
|
358
444
|
const icon = report.reproducible ? chalk.green('✓') : chalk.yellow('!');
|
|
359
445
|
console.log(`${icon} Hosted reproducibility: ${report.reproducible ? 'reproducible' : 'not established'}`);
|
|
360
|
-
console.log(` ${chalk.dim('Runs:')} ${report.analyzable_runs}/${report.requested_runs} analyzable`);
|
|
446
|
+
console.log(` ${chalk.dim('Runs:')} ${report.analyzable_runs}/${report.requested_runs} analyzable; ${report.launched_runs} launched`);
|
|
361
447
|
console.log(` ${chalk.dim('Plan variants:')} ${report.plan_variants.length}`);
|
|
448
|
+
console.log(` ${chalk.dim('Exact plan replay:')} ${report.plan_replay_verified ? 'verified' : 'not verified'}`);
|
|
449
|
+
console.log(` ${chalk.dim('Independent planner reproducibility:')} not tested`);
|
|
362
450
|
console.log(` ${chalk.dim('Assessment variants:')} ${report.assessment_variants.length}`);
|
|
363
451
|
if (report.plan_drift_cell_ids.length > 0) {
|
|
364
452
|
console.log(` ${chalk.dim('Plan drift:')} ${report.plan_drift_cell_ids.length} stable cell id(s)`);
|
|
@@ -392,6 +480,9 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
|
|
|
392
480
|
base: options.base,
|
|
393
481
|
head: options.head,
|
|
394
482
|
idempotencyKey: hostedReproducibilityRunKey(seriesKey, repository.fullName, options.base, options.head, index + 1),
|
|
483
|
+
replayPlanFrom: index === 0
|
|
484
|
+
? undefined
|
|
485
|
+
: runs[0]?.state.runId,
|
|
395
486
|
wait: false,
|
|
396
487
|
}, auth.token);
|
|
397
488
|
let state = started.state;
|
|
@@ -406,12 +497,25 @@ export async function verifyHostedReproducibilityCommand(repositoryArg, options)
|
|
|
406
497
|
timedOut = waited.timedOut;
|
|
407
498
|
}
|
|
408
499
|
runs.push({ state, timedOut });
|
|
500
|
+
if (index === 0
|
|
501
|
+
&& !isHostedReproducibilityReplaySource(runs[0], {
|
|
502
|
+
seriesKey,
|
|
503
|
+
repository: repository.fullName,
|
|
504
|
+
baseCommit: options.base,
|
|
505
|
+
headCommit: options.head,
|
|
506
|
+
})) {
|
|
507
|
+
if (!options.json) {
|
|
508
|
+
console.error(chalk.yellow('Source run is not replayable; later runs were not launched.'));
|
|
509
|
+
}
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
409
512
|
}
|
|
410
513
|
const report = analyzeHostedReproducibility(runs, {
|
|
411
514
|
seriesKey,
|
|
412
515
|
repository: repository.fullName,
|
|
413
516
|
baseCommit: options.base,
|
|
414
517
|
headCommit: options.head,
|
|
518
|
+
requestedRuns,
|
|
415
519
|
});
|
|
416
520
|
if (options.json) {
|
|
417
521
|
process.stdout.write(`${JSON.stringify(withSchema('cloud-verifier', {
|
|
@@ -2,6 +2,7 @@ export interface HostedStartOptions {
|
|
|
2
2
|
base: string;
|
|
3
3
|
head: string;
|
|
4
4
|
idempotencyKey?: string;
|
|
5
|
+
replayPlanFrom?: string;
|
|
5
6
|
account?: string;
|
|
6
7
|
wait?: boolean;
|
|
7
8
|
interval?: string;
|
|
@@ -41,7 +42,7 @@ export declare function parseHostedRepository(value: string): {
|
|
|
41
42
|
repository: string;
|
|
42
43
|
fullName: string;
|
|
43
44
|
};
|
|
44
|
-
export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string): string;
|
|
45
|
+
export declare function defaultHostedIdempotencyKey(repository: string, baseCommit: string, headCommit: string, replayPlanFrom?: string): string;
|
|
45
46
|
export declare function hostedPollingOptions(interval: string | undefined, timeout: string | undefined, onStatus?: (state: HostedRunState) => void): HostedPollOptions;
|
|
46
47
|
export declare function startHostedVerification(repositoryArg: string, options: HostedStartOptions, token: string): Promise<{
|
|
47
48
|
state: HostedRunState;
|
|
@@ -121,9 +121,11 @@ function requireCommit(value, flag) {
|
|
|
121
121
|
}
|
|
122
122
|
return value;
|
|
123
123
|
}
|
|
124
|
-
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit) {
|
|
124
|
+
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom) {
|
|
125
125
|
const digest = createHash('sha256')
|
|
126
|
-
.update(JSON.stringify(
|
|
126
|
+
.update(JSON.stringify(replayPlanFrom
|
|
127
|
+
? [repository, baseCommit, headCommit, replayPlanFrom]
|
|
128
|
+
: [repository, baseCommit, headCommit]))
|
|
127
129
|
.digest('hex');
|
|
128
130
|
return `cli:${digest}`;
|
|
129
131
|
}
|
|
@@ -153,8 +155,12 @@ export async function startHostedVerification(repositoryArg, options, token) {
|
|
|
153
155
|
const repository = parseHostedRepository(repositoryArg);
|
|
154
156
|
const baseCommit = requireCommit(options.base, '--base');
|
|
155
157
|
const headCommit = requireCommit(options.head, '--head');
|
|
158
|
+
const replayPlanFrom = options.replayPlanFrom;
|
|
159
|
+
if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
|
|
160
|
+
throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
|
|
161
|
+
}
|
|
156
162
|
const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
|
|
157
|
-
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit));
|
|
163
|
+
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom));
|
|
158
164
|
const response = await haystackJson('/api/agent/cloud-verifier/runs', token, {
|
|
159
165
|
method: 'POST',
|
|
160
166
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -164,6 +170,9 @@ export async function startHostedVerification(repositoryArg, options, token) {
|
|
|
164
170
|
baseCommit,
|
|
165
171
|
headCommit,
|
|
166
172
|
idempotencyKey,
|
|
173
|
+
...(replayPlanFrom
|
|
174
|
+
? { replayPlanFromRunId: replayPlanFrom }
|
|
175
|
+
: {}),
|
|
167
176
|
}),
|
|
168
177
|
});
|
|
169
178
|
const state = parseHostedRunState(response);
|
|
@@ -200,7 +209,7 @@ function delay(milliseconds) {
|
|
|
200
209
|
export async function waitForHostedVerification(runId, token, options) {
|
|
201
210
|
const startedAt = Date.now();
|
|
202
211
|
let lastStatus;
|
|
203
|
-
|
|
212
|
+
for (;;) {
|
|
204
213
|
const state = await getHostedVerification(runId, token);
|
|
205
214
|
if (state.status !== lastStatus) {
|
|
206
215
|
options.onStatus?.(state);
|
package/dist/index.js
CHANGED
|
@@ -219,6 +219,7 @@ hostedVerify
|
|
|
219
219
|
.option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
|
|
220
220
|
.option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
|
|
221
221
|
.option('--idempotency-key <key>', 'Stable retry key (default: deterministic from repository and commits)')
|
|
222
|
+
.option('--replay-plan-from <run-id>', 'Reuse the exact validated risk plan from a completed owned run')
|
|
222
223
|
.option('--account <login>', 'Use a specific saved Haystack account')
|
|
223
224
|
.option('--no-wait', 'Return after the production workflow is queued')
|
|
224
225
|
.option('--interval <seconds>', 'Polling interval while waiting (default 5)')
|
|
@@ -228,9 +229,12 @@ hostedVerify
|
|
|
228
229
|
The default idempotency key makes an exact retry return the original run. Pass
|
|
229
230
|
--idempotency-key with a new stable key only when you intentionally want a
|
|
230
231
|
fresh production execution of the same commit pair.
|
|
232
|
+
--replay-plan-from requires the same repository and exact commit pair and emits
|
|
233
|
+
a schema-9 terminal receipt with replay provenance.
|
|
231
234
|
|
|
232
235
|
Examples:
|
|
233
236
|
haystack verify hosted start owner/repo --base <sha> --head <sha>
|
|
237
|
+
haystack verify hosted start owner/repo --base <sha> --head <sha> --replay-plan-from cv_<48 lowercase hex characters>
|
|
234
238
|
haystack verify hosted start owner/repo --base <sha> --head <sha> --no-wait --json
|
|
235
239
|
`)
|
|
236
240
|
.action(async (repository, _options, cmd) => {
|
|
@@ -258,7 +262,7 @@ Example:
|
|
|
258
262
|
});
|
|
259
263
|
hostedVerify
|
|
260
264
|
.command('reproducibility')
|
|
261
|
-
.description('
|
|
265
|
+
.description('Replay one exact hosted risk plan and report executor or assessment drift')
|
|
262
266
|
.argument('<repository>', 'Exact GitHub owner/repository name')
|
|
263
267
|
.option('--base <sha>', 'Required: exact 40-character lowercase base commit SHA')
|
|
264
268
|
.option('--head <sha>', 'Required: exact 40-character lowercase head commit SHA')
|
|
@@ -269,10 +273,10 @@ hostedVerify
|
|
|
269
273
|
.option('--timeout <minutes>', 'Maximum wait per run (default 35)')
|
|
270
274
|
.option('--json', 'Versioned machine-readable reproducibility report')
|
|
271
275
|
.addHelpText('after', `
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
+
The first run creates a fresh risk plan. Every later run replays that exact
|
|
277
|
+
validated plan against the same repository and commit pair with a distinct,
|
|
278
|
+
series-derived idempotency key. A reproducible verdict requires schema-9 replay
|
|
279
|
+
provenance, stable assessment outcomes, and no inconclusive cells.
|
|
276
280
|
|
|
277
281
|
Example:
|
|
278
282
|
haystack verify hosted reproducibility owner/repo --base <sha> --head <sha> --runs 3
|