@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 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
- Repeat the same exact production launch to detect planner or assessment drift:
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
- Each run gets a distinct idempotency key derived from one printed series key.
130
- Pass `--series-key <key>` to resume the same series without creating duplicate
131
- runs. The report compares stable risk-cell, capability, and comparison IDs.
132
- Plan drift, assessment drift, timeouts, unsupported receipts, and inconclusive
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
- seriesKey: string;
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 plannerReproducible = parsed.length === runs.length && planVariants.length === 1;
321
- const assessmentReproducible = plannerReproducible && assessmentVariants.length === 1;
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: runs.length,
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
- planner_reproducible: plannerReproducible,
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: assessmentReproducible && inconclusiveCellIds.length === 0,
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([repository, baseCommit, headCommit]))
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
- while (true) {
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('Repeat an exact hosted verification and report plan or assessment drift')
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
- Every run uses the same repository and exact commit pair with a distinct,
273
- series-derived idempotency key. The report compares stable risk-cell identities
274
- and deterministic assessment outcomes; inconclusive cells never count as a
275
- reproducible verdict.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.24",
3
+ "version": "0.15.25",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {