@haystackeditor/cli 0.15.31 → 0.17.0

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 (136) hide show
  1. package/README.md +141 -61
  2. package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
  3. package/dist/assets/hooks/scripts/post-commit.sh +3 -0
  4. package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
  5. package/dist/assets/hooks/scripts/pre-push.sh +3 -0
  6. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  7. package/dist/commands/case-batch-contract.js +694 -0
  8. package/dist/commands/case-batch.js +1011 -0
  9. package/dist/commands/cloud-verifier-identity-census.js +5 -2
  10. package/dist/commands/combination-search-hook.js +139 -0
  11. package/dist/commands/dismiss.js +2 -1
  12. package/dist/commands/hooks.js +66 -7
  13. package/dist/commands/install-session-hooks.js +131 -44
  14. package/dist/commands/policy.js +32 -45
  15. package/dist/commands/precompute-delivery.js +6 -1
  16. package/dist/commands/scaffold-provisional-universe.js +8 -10
  17. package/dist/commands/setup.js +32 -10
  18. package/dist/commands/submit.js +39 -71
  19. package/dist/commands/telemetry.js +17 -2
  20. package/dist/commands/triage.js +2 -1
  21. package/dist/commands/verify-explore.js +1 -0
  22. package/dist/commands/verify-hosted-mcp.js +3 -12
  23. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  24. package/dist/commands/verify-hosted.js +66 -168
  25. package/dist/commands/verify-precompute.js +64 -11
  26. package/dist/commands/verify.js +51 -139
  27. package/dist/index.js +290 -366
  28. package/dist/lazy.js +8 -0
  29. package/dist/schema.js +2 -1
  30. package/dist/tools/detect.js +3 -24
  31. package/dist/triage/astra.js +199 -0
  32. package/dist/triage/prompts.js +145 -179
  33. package/dist/triage/runner.js +104 -300
  34. package/dist/triage/types.js +2 -2
  35. package/dist/types.js +2 -6
  36. package/dist/utils/auth.js +14 -2
  37. package/dist/utils/git.js +60 -29
  38. package/dist/utils/github-api.js +14 -1
  39. package/dist/utils/haystack-api.js +38 -7
  40. package/dist/utils/hooks.js +43 -6
  41. package/dist/utils/prompter.js +25 -10
  42. package/dist/utils/safe-write.js +31 -0
  43. package/dist/utils/secret-paths.js +115 -0
  44. package/dist/utils/secrets.js +0 -1
  45. package/dist/utils/telemetry.js +34 -14
  46. package/dist/utils/update-check.js +151 -0
  47. package/package.json +19 -14
  48. package/schemas/case-batch.v1.json +235 -0
  49. package/schemas/cloud-verifier.v1.json +55 -100
  50. package/schemas/submit.v1.json +4 -2
  51. package/dist/commands/ask.d.ts +0 -14
  52. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  53. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  54. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  55. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  56. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  57. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  58. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  59. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  60. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  61. package/dist/commands/config.d.ts +0 -46
  62. package/dist/commands/design-verify.d.ts +0 -33
  63. package/dist/commands/dismiss.d.ts +0 -29
  64. package/dist/commands/hooks.d.ts +0 -13
  65. package/dist/commands/inbox.d.ts +0 -65
  66. package/dist/commands/init.d.ts +0 -10
  67. package/dist/commands/install-session-hooks.d.ts +0 -17
  68. package/dist/commands/login.d.ts +0 -8
  69. package/dist/commands/mcp.d.ts +0 -1
  70. package/dist/commands/policy.d.ts +0 -31
  71. package/dist/commands/pr-status.d.ts +0 -144
  72. package/dist/commands/pr.d.ts +0 -40
  73. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  74. package/dist/commands/precompute-delivery.d.ts +0 -20
  75. package/dist/commands/prepare-universe-review.d.ts +0 -115
  76. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  77. package/dist/commands/request-review.d.ts +0 -26
  78. package/dist/commands/review.d.ts +0 -25
  79. package/dist/commands/rules.d.ts +0 -4
  80. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  81. package/dist/commands/schema-cmd.d.ts +0 -2
  82. package/dist/commands/setup.d.ts +0 -28
  83. package/dist/commands/skills.d.ts +0 -8
  84. package/dist/commands/status.d.ts +0 -4
  85. package/dist/commands/submit.d.ts +0 -30
  86. package/dist/commands/system-map.d.ts +0 -42
  87. package/dist/commands/telemetry.d.ts +0 -53
  88. package/dist/commands/tokens.d.ts +0 -14
  89. package/dist/commands/triage.d.ts +0 -35
  90. package/dist/commands/verify-core.d.ts +0 -449
  91. package/dist/commands/verify-core.js +0 -789
  92. package/dist/commands/verify-explore.d.ts +0 -14
  93. package/dist/commands/verify-history.d.ts +0 -14
  94. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  95. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  96. package/dist/commands/verify-hosted.d.ts +0 -92
  97. package/dist/commands/verify-mcp.d.ts +0 -8
  98. package/dist/commands/verify-mcp.js +0 -517
  99. package/dist/commands/verify-ops.d.ts +0 -158
  100. package/dist/commands/verify-ops.js +0 -1148
  101. package/dist/commands/verify-precompute.d.ts +0 -30
  102. package/dist/commands/verify-reproducibility.d.ts +0 -85
  103. package/dist/commands/verify-reproducibility.js +0 -494
  104. package/dist/commands/verify-reseal.d.ts +0 -9
  105. package/dist/commands/verify-reseal.js +0 -148
  106. package/dist/commands/verify-sandboxes.d.ts +0 -95
  107. package/dist/commands/verify-sandboxes.js +0 -352
  108. package/dist/commands/verify.d.ts +0 -28
  109. package/dist/commands/webhooks.d.ts +0 -30
  110. package/dist/index.d.ts +0 -22
  111. package/dist/schema.d.ts +0 -28
  112. package/dist/states.d.ts +0 -29
  113. package/dist/tools/detect.d.ts +0 -50
  114. package/dist/triage/prompts.d.ts +0 -24
  115. package/dist/triage/runner.d.ts +0 -34
  116. package/dist/triage/types.d.ts +0 -42
  117. package/dist/types/verify-history.d.ts +0 -38
  118. package/dist/types.d.ts +0 -1684
  119. package/dist/utils/action-output.d.ts +0 -24
  120. package/dist/utils/analysis-api.d.ts +0 -187
  121. package/dist/utils/auth.d.ts +0 -79
  122. package/dist/utils/config.d.ts +0 -24
  123. package/dist/utils/design-verifier-api.d.ts +0 -208
  124. package/dist/utils/design-verifier-history.d.ts +0 -46
  125. package/dist/utils/design-verifier-result.d.ts +0 -74
  126. package/dist/utils/detect.d.ts +0 -43
  127. package/dist/utils/git.d.ts +0 -135
  128. package/dist/utils/github-api.d.ts +0 -104
  129. package/dist/utils/haystack-api.d.ts +0 -37
  130. package/dist/utils/hooks.d.ts +0 -12
  131. package/dist/utils/pending-state.d.ts +0 -40
  132. package/dist/utils/pr-ref.d.ts +0 -27
  133. package/dist/utils/prompter.d.ts +0 -85
  134. package/dist/utils/secrets.d.ts +0 -47
  135. package/dist/utils/telemetry.d.ts +0 -19
  136. /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
@@ -76,98 +76,64 @@ export function parseHostedRunState(value, expectedRunId) {
76
76
  if (headCommit && !COMMIT_SHA.test(headCommit)) {
77
77
  throw new Error('Hosted verifier response has an invalid headCommit.');
78
78
  }
79
- const outputValue = response.output;
80
- const output = outputValue === undefined
81
- ? undefined
82
- : requireRecord(outputValue, 'Hosted verifier output');
83
79
  const resultValue = response.result;
84
- const fleet = response.provider === 'haystack';
85
- if (response.provider !== undefined && !fleet) {
80
+ if (response.provider !== 'haystack') {
86
81
  throw new Error('Hosted verifier returned an unsupported provider.');
87
82
  }
88
- if (fleet && response.version !== FLEET_PROJECTION_VERSION) {
83
+ if (response.version !== FLEET_PROJECTION_VERSION) {
89
84
  throw new Error('Hosted verifier returned an unsupported fleet projection version.');
90
85
  }
91
- const result = fleet || resultValue === undefined
92
- ? undefined
93
- : parseHostedRunnerResult(resultValue);
94
86
  const createdAt = optionalTimestamp(response, 'createdAt');
95
87
  const completedAt = optionalTimestamp(response, 'completedAt');
96
88
  let progress;
97
89
  let fleetResult;
98
- if (fleet) {
99
- if (response.publicRunId !== runId || !repository || !baseCommit || !headCommit || !createdAt || output) {
100
- throw new Error('Hosted fleet projection is missing its immutable run identity.');
90
+ if (response.publicRunId !== runId || !repository || !baseCommit || !headCommit || !createdAt) {
91
+ throw new Error('Hosted fleet projection is missing its immutable run identity.');
92
+ }
93
+ if (response.progress !== undefined && response.progress !== null) {
94
+ progress = requireRecord(response.progress, 'Hosted fleet progress');
95
+ if (progress.version !== 'dv-verify-progress-v1' || progress.publicRunId !== runId) {
96
+ throw new Error('Hosted fleet progress does not match the requested run.');
101
97
  }
102
- if (response.progress !== undefined && response.progress !== null) {
103
- progress = requireRecord(response.progress, 'Hosted fleet progress');
104
- if (progress.version !== 'dv-verify-progress-v1' || progress.publicRunId !== runId) {
105
- throw new Error('Hosted fleet progress does not match the requested run.');
106
- }
107
- requireString(progress, 'stage');
108
- requireString(progress, 'status');
98
+ requireString(progress, 'stage');
99
+ requireString(progress, 'status');
100
+ }
101
+ if (resultValue !== undefined && resultValue !== null) {
102
+ fleetResult = requireRecord(resultValue, 'Hosted fleet result');
103
+ const mapping = requireRecord(fleetResult.mapping, 'Hosted fleet result mapping');
104
+ const provenance = requireRecord(fleetResult.provenance, 'Hosted fleet provenance');
105
+ if (fleetResult.version !== 'dv-verify-result-v1' || fleetResult.provider !== 'haystack'
106
+ || mapping.publicRunId !== runId || provenance.baseSha !== baseCommit || provenance.headSha !== headCommit) {
107
+ throw new Error('Hosted fleet result does not match the requested run and commits.');
109
108
  }
110
- if (resultValue !== undefined && resultValue !== null) {
111
- fleetResult = requireRecord(resultValue, 'Hosted fleet result');
112
- const mapping = requireRecord(fleetResult.mapping, 'Hosted fleet result mapping');
113
- const provenance = requireRecord(fleetResult.provenance, 'Hosted fleet provenance');
114
- if (fleetResult.version !== 'dv-verify-result-v1' || fleetResult.provider !== 'haystack'
115
- || mapping.publicRunId !== runId || provenance.baseSha !== baseCommit || provenance.headSha !== headCommit) {
116
- throw new Error('Hosted fleet result does not match the requested run and commits.');
117
- }
118
- // CONTRACT-GAP: P0 currently couples its canonical parsers to runner-only
119
- // modules outside this published CLI. The server validates the complete
120
- // P0 result; the CLI only checks the fields it presents, retaining the
121
- // original document instead of defining a second P0 wire contract.
122
- requireString(fleetResult, 'lifecycle', /^(complete|failed|cancelled|budget-stopped|account-action)$/);
123
- requireString(fleetResult, 'runHealth', /^(healthy|degraded|infrastructure-error|not-executed)$/);
124
- const cleanup = requireRecord(fleetResult.cleanup, 'Hosted fleet cleanup');
125
- requireString(cleanup, 'status', /^(complete|pending)$/);
126
- if (!Array.isArray(cleanup.pending) || !Array.isArray(fleetResult.findings)) {
127
- throw new Error('Hosted fleet result has invalid findings or cleanup.');
128
- }
129
- for (const value of fleetResult.findings) {
130
- const finding = requireRecord(value, 'Hosted fleet finding');
131
- requireString(finding, 'findingId');
132
- requireString(finding, 'caseId');
133
- requireString(finding, 'judgment');
134
- }
135
- if (fleetResult.savedChecks !== undefined) {
136
- parseHostedSavedChecks(fleetResult.savedChecks, runId, headCommit);
137
- }
138
- requireString(provenance, 'intent', /^(user|inferred)$/);
139
- if (status === 'complete' && fleetResult.lifecycle !== 'complete') {
140
- throw new Error('Hosted fleet lifecycle disagrees with its completed job.');
141
- }
109
+ // CONTRACT-GAP: P0 currently couples its canonical parsers to runner-only
110
+ // modules outside this published CLI. The server validates the complete
111
+ // P0 result; the CLI only checks the fields it presents, retaining the
112
+ // original document instead of defining a second P0 wire contract.
113
+ requireString(fleetResult, 'lifecycle', /^(complete|failed|cancelled|budget-stopped|account-action)$/);
114
+ requireString(fleetResult, 'runHealth', /^(healthy|degraded|infrastructure-error|not-executed)$/);
115
+ const cleanup = requireRecord(fleetResult.cleanup, 'Hosted fleet cleanup');
116
+ requireString(cleanup, 'status', /^(complete|pending)$/);
117
+ if (!Array.isArray(cleanup.pending) || !Array.isArray(fleetResult.findings)) {
118
+ throw new Error('Hosted fleet result has invalid findings or cleanup.');
142
119
  }
143
- if (status === 'complete' && !fleetResult) {
144
- throw new Error('Hosted fleet run completed without a result receipt.');
120
+ for (const value of fleetResult.findings) {
121
+ const finding = requireRecord(value, 'Hosted fleet finding');
122
+ requireString(finding, 'findingId');
123
+ requireString(finding, 'caseId');
124
+ requireString(finding, 'judgment');
145
125
  }
146
- }
147
- else if (status === 'complete') {
148
- // Two terminal shapes exist: the Workflow receipt envelope (`output`)
149
- // and the runner-queue result envelope (`result`). Exactly one must
150
- // validate.
151
- if (result) {
152
- if (!repository || !baseCommit || !headCommit || !createdAt) {
153
- throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
154
- }
126
+ if (fleetResult.savedChecks !== undefined) {
127
+ parseHostedSavedChecks(fleetResult.savedChecks, runId, headCommit);
155
128
  }
156
- else if (!repository
157
- || !baseCommit
158
- || !headCommit
159
- || !createdAt
160
- || !completedAt
161
- || !output
162
- || output.runId !== runId
163
- || output.repository !== repository
164
- || output.baseCommit !== baseCommit
165
- || output.headCommit !== headCommit
166
- || output.sourceOfflineReady !== true
167
- || output.sandboxDeleted !== true) {
168
- throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
129
+ requireString(provenance, 'intent', /^(user|inferred)$/);
130
+ if (status === 'complete' && fleetResult.lifecycle !== 'complete') {
131
+ throw new Error('Hosted fleet lifecycle disagrees with its completed job.');
169
132
  }
170
133
  }
134
+ if (status === 'complete' && !fleetResult) {
135
+ throw new Error('Hosted fleet run completed without a result receipt.');
136
+ }
171
137
  return {
172
138
  runId,
173
139
  status,
@@ -176,37 +142,13 @@ export function parseHostedRunState(value, expectedRunId) {
176
142
  headCommit,
177
143
  createdAt,
178
144
  completedAt,
179
- output,
180
- result,
181
- ...(fleet ? { provider: 'haystack', version: FLEET_PROJECTION_VERSION, progress, fleetResult } : {}),
145
+ provider: 'haystack',
146
+ version: FLEET_PROJECTION_VERSION,
147
+ progress,
148
+ fleetResult,
182
149
  error: optionalString(response, 'error'),
183
150
  };
184
151
  }
185
- function parseHostedRunnerResult(value) {
186
- const record = requireRecord(value, 'Hosted verifier runner result');
187
- const regressionsValue = record.regressions;
188
- if (typeof record.verdict !== 'string'
189
- || typeof record.summary !== 'string'
190
- || !Number.isInteger(record.casesTotal)
191
- || !Number.isInteger(record.casesFailed)
192
- || typeof record.outputLocation !== 'string'
193
- || typeof record.wallSeconds !== 'number'
194
- || !Array.isArray(regressionsValue)
195
- || !regressionsValue.every(item => !!item && typeof item === 'object' && !Array.isArray(item)
196
- && typeof item.caseId === 'string'
197
- && typeof item.title === 'string')) {
198
- throw new Error('Hosted verifier runner result is malformed.');
199
- }
200
- return {
201
- verdict: record.verdict,
202
- summary: record.summary,
203
- casesTotal: record.casesTotal,
204
- casesFailed: record.casesFailed,
205
- regressions: regressionsValue,
206
- outputLocation: record.outputLocation,
207
- wallSeconds: record.wallSeconds,
208
- };
209
- }
210
152
  export function parseHostedRepository(value) {
211
153
  const parts = value.split('/');
212
154
  if (parts.length !== 2
@@ -303,15 +245,11 @@ function parseHostedSavedChecks(value, runId, headSha) {
303
245
  return saved;
304
246
  }
305
247
  function savedCheckDisplay(value) {
248
+ // eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
306
249
  return String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
307
250
  }
308
- function requireCommit(value, flag) {
309
- if (typeof value !== 'string' || !COMMIT_SHA.test(value)) {
310
- throw new Error(`${flag} must be an exact 40-character lowercase commit SHA.`);
311
- }
312
- return value;
313
- }
314
251
  function requireRevision(value, flag) {
252
+ // eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
315
253
  if (typeof value !== 'string' || value.length === 0 || value.length > 1_024 || /[\u0000-\u0020\u007f]/.test(value)) {
316
254
  throw new Error(`${flag} must be a non-empty Git revision of at most 1024 characters without whitespace or control characters.`);
317
255
  }
@@ -365,13 +303,9 @@ export function readHostedIntentFile(intentFile) {
365
303
  intended_outcomes: record.intended_outcomes,
366
304
  };
367
305
  }
368
- export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom, fleetRequest) {
306
+ export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, intent) {
369
307
  const digest = createHash('sha256')
370
- .update(JSON.stringify(fleetRequest
371
- ? [fleetRequest.provider, repository, baseCommit, headCommit, fleetRequest.intent ?? null]
372
- : replayPlanFrom
373
- ? [repository, baseCommit, headCommit, replayPlanFrom]
374
- : [repository, baseCommit, headCommit]))
308
+ .update(JSON.stringify(['haystack', repository, baseCommit, headCommit, intent ?? null]))
375
309
  .digest('hex');
376
310
  return `cli:${digest}`;
377
311
  }
@@ -398,7 +332,7 @@ export function hostedPollingOptions(interval, timeout, onStatus) {
398
332
  };
399
333
  }
400
334
  export async function startHostedVerification(repositoryArg, options, token) {
401
- const { repository, launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
335
+ const { launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
402
336
  const path = '/api/agent/cloud-verifier/runs';
403
337
  const response = await fetch(haystackApiUrl(path), {
404
338
  method: 'POST',
@@ -424,49 +358,27 @@ export async function startHostedVerification(repositoryArg, options, token) {
424
358
  throw error;
425
359
  }
426
360
  const state = parseHostedRunState(await response.json());
427
- if (options.provider === 'haystack' && state.provider !== 'haystack') {
428
- throw new Error('Hosted verifier did not acknowledge the fleet-pinned request.');
429
- }
430
- return {
431
- state: {
432
- ...state,
433
- repository: state.repository ?? repository.fullName,
434
- baseCommit: state.baseCommit ?? launchBody.baseCommit,
435
- headCommit: state.headCommit ?? launchBody.headCommit,
436
- },
437
- idempotencyKey,
438
- };
361
+ return { state, idempotencyKey };
439
362
  }
440
363
  function prepareHostedLaunch(repositoryArg, options) {
441
364
  const repository = parseHostedRepository(repositoryArg);
442
- const fleet = options.provider === 'haystack';
443
- const validateRevision = fleet ? requireRevision : requireCommit;
444
- const baseCommit = validateRevision(options.base, '--base');
445
- const headCommit = validateRevision(options.head, '--head');
446
- const intent = fleet && options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
447
- if (fleet && intent && [intent.problem, intent.goal, ...intent.intended_outcomes].some(value => /[\u0000-\u001f\u007f]/.test(value))) {
365
+ const baseCommit = requireRevision(options.base, '--base');
366
+ const headCommit = requireRevision(options.head, '--head');
367
+ const intent = options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
368
+ // eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
369
+ if (intent && [intent.problem, intent.goal, ...intent.intended_outcomes].some(value => /[\u0000-\u001f\u007f]/.test(value))) {
448
370
  throw new Error('Fleet --intent-file fields must be single-line text without control characters.');
449
371
  }
450
- const replayPlanFrom = options.replayPlanFrom;
451
- if (fleet && replayPlanFrom !== undefined) {
452
- throw new Error('--replay-plan-from is available through the explicit hosted start alias.');
453
- }
454
- if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
455
- throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
456
- }
457
372
  const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
458
- ?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom, fleet ? { provider: 'haystack', intent } : undefined));
373
+ ?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, intent));
459
374
  const launchBody = {
460
- ...(fleet ? { provider: 'haystack' } : {}),
375
+ provider: 'haystack',
461
376
  owner: repository.owner,
462
377
  repository: repository.repository,
463
378
  baseCommit,
464
379
  headCommit,
465
380
  idempotencyKey,
466
381
  ...(intent ? { intent } : {}),
467
- ...(replayPlanFrom
468
- ? { replayPlanFromRunId: replayPlanFrom }
469
- : {}),
470
382
  };
471
383
  return { repository, launchBody, idempotencyKey };
472
384
  }
@@ -526,9 +438,8 @@ export function buildHostedJson(operation, state, extras = {}) {
526
438
  : {}),
527
439
  ...(extras.nextCommand ? { next_command: extras.nextCommand } : {}),
528
440
  ...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
529
- ...(state.output ? { output: state.output } : {}),
530
- ...(state.result ? { result: state.result } : {}),
531
- ...(state.provider ? { provider: state.provider, version: state.version } : {}),
441
+ provider: state.provider,
442
+ version: state.version,
532
443
  ...(state.progress ? { progress: state.progress } : {}),
533
444
  ...(state.fleetResult ? { result: state.fleetResult } : {}),
534
445
  ...(state.error ? { error: state.error } : {}),
@@ -550,13 +461,11 @@ function printHostedState(operation, state, options) {
550
461
  }), null, 2)}\n`);
551
462
  return;
552
463
  }
553
- const icon = state.provider === 'haystack' && state.status === 'complete'
464
+ const icon = state.status === 'complete'
554
465
  ? chalk.cyan('•')
555
- : state.status === 'complete'
556
- ? chalk.green('✓')
557
- : TERMINAL_STATUSES.has(state.status)
558
- ? chalk.red('✗')
559
- : chalk.cyan('•');
466
+ : TERMINAL_STATUSES.has(state.status)
467
+ ? chalk.red('✗')
468
+ : chalk.cyan('•');
560
469
  console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
561
470
  if (state.repository) {
562
471
  console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
@@ -564,15 +473,6 @@ function printHostedState(operation, state, options) {
564
473
  if (state.baseCommit && state.headCommit) {
565
474
  console.log(` ${chalk.dim('Commits:')} ${state.baseCommit.slice(0, 12)} → ${state.headCommit.slice(0, 12)}`);
566
475
  }
567
- if (state.result) {
568
- const passed = state.result.casesTotal - state.result.casesFailed;
569
- console.log(` ${chalk.dim('Verdict:')} ${state.result.verdict} `
570
- + `(${passed}/${state.result.casesTotal} cases clean, ${Math.round(state.result.wallSeconds)}s)`);
571
- for (const regression of state.result.regressions) {
572
- console.log(` ${chalk.red('✗')} ${regression.caseId}: ${regression.title}`);
573
- }
574
- console.log(` ${chalk.dim('Artifacts:')} ${state.result.outputLocation}`);
575
- }
576
476
  if (state.progress) {
577
477
  console.log(` ${chalk.dim('Progress:')} ${state.progress.stage} (${state.progress.status})`);
578
478
  }
@@ -657,9 +557,7 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
657
557
  repo: repository.repository,
658
558
  });
659
559
  if (!options.json) {
660
- console.error(chalk.dim(options.provider === 'haystack'
661
- ? 'Submitting repository revisions to the hosted fleet verifier…'
662
- : 'Submitting the exact commit pair to the hosted verifier…'));
560
+ console.error(chalk.dim('Submitting repository revisions to the hosted fleet verifier…'));
663
561
  }
664
562
  const started = await startHostedVerification(repositoryArg, options, auth.token);
665
563
  const selectedAccount = auth.token.startsWith('hsk_live_')
@@ -7,9 +7,11 @@ import { tmpdir } from 'node:os';
7
7
  import chalk from 'chalk';
8
8
  import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
9
9
  import { findGitRoot, parseRemoteUrl, resolveDiffBaseRef, } from '../utils/git.js';
10
- import { HaystackApiError, haystackJson } from '../utils/haystack-api.js';
10
+ import { HaystackApiError, haystackJson, REPO_NOT_ENTITLED_CODE, SUBSCRIPTION_REQUIRED_CODE, } from '../utils/haystack-api.js';
11
11
  import { trackPrecomputeEvent } from '../utils/telemetry.js';
12
+ import { findSecretsInPatch, isSecretBearingPath } from '../utils/secret-paths.js';
12
13
  import { startPrecomputeDelivery } from './precompute-delivery.js';
14
+ import { captureCombinationSearchHook, readCombinationSearchHookStatus } from './combination-search-hook.js';
13
15
  // --hook has five seconds to capture the exact source and publish local custody.
14
16
  // Its detached sender owns the bounded network delivery and acknowledgement.
15
17
  // An explicit invocation still waits synchronously for the server response.
@@ -48,13 +50,16 @@ function remainingMs(deadline) {
48
50
  function gitOutput(args, gitRoot, deadline, options = {}) {
49
51
  const timeout = Math.max(1, Math.min(remainingMs(deadline), options.timeoutCapMs ?? HOOK_WALL_MS));
50
52
  try {
51
- return execFileSync('git', args, {
53
+ const output = execFileSync('git', args, {
52
54
  cwd: gitRoot,
53
55
  encoding: 'utf8',
54
56
  env: options.env ?? process.env,
55
57
  stdio: ['pipe', 'pipe', 'pipe'],
56
58
  timeout,
57
- }).trim();
59
+ ...(options.input === undefined ? {} : { input: options.input }),
60
+ ...(options.maxBuffer === undefined ? {} : { maxBuffer: options.maxBuffer }),
61
+ });
62
+ return options.raw ? output : output.trim();
58
63
  }
59
64
  catch {
60
65
  throw new PrecomputeFailure('Could not derive the repository snapshot.');
@@ -110,7 +115,41 @@ function deriveBaseSha(gitRoot, deadline) {
110
115
  }
111
116
  return baseSha;
112
117
  }
113
- function deriveWorktreeTreeSha(gitRoot, deadline) {
118
+ // The -U0 scan text omits binary content and context, so it stays far below
119
+ // the uploaded patch; this only guards against git's 1 MiB default.
120
+ const SCAN_PATCH_MAX_BYTES = 256 * 1024 * 1024;
121
+ /**
122
+ * Withholds secret-bearing changes from the snapshot index before the tree is
123
+ * written, so the tree, work commit, uploaded patch and combination-search
124
+ * patches all agree. A withheld path keeps its base version (or is absent when
125
+ * base has none). Every withheld path is named on stderr; secret values never are.
126
+ */
127
+ function withholdSecretBearingChanges(gitRoot, baseSha, indexEnv, deadline) {
128
+ const changed = gitOutput(['diff', '--cached', '--no-renames', '--diff-filter=d', '--name-only', '-z', baseSha, '--'], gitRoot, deadline, { env: indexEnv, maxBuffer: SCAN_PATCH_MAX_BYTES, raw: true }).split('\0').filter(path => path.length > 0);
129
+ const withheld = new Map();
130
+ for (const path of changed) {
131
+ if (isSecretBearingPath(path))
132
+ withheld.set(path, 'secret-bearing file name');
133
+ }
134
+ if (changed.length > withheld.size) {
135
+ const patch = gitOutput(['-c', 'core.quotePath=false', 'diff', '--cached', '--no-renames', '--diff-filter=d', '-U0',
136
+ '--no-color', '--no-ext-diff', '--no-textconv',
137
+ // pathFromDiffHeader expects `a/` and `b/`; pin them so diff.noprefix
138
+ // or diff.mnemonicPrefix in the user's git config cannot break parsing.
139
+ '--src-prefix=a/', '--dst-prefix=b/', baseSha, '--'], gitRoot, deadline, { env: indexEnv, maxBuffer: SCAN_PATCH_MAX_BYTES });
140
+ for (const [path, pattern] of findSecretsInPatch(patch)) {
141
+ if (!withheld.has(path))
142
+ withheld.set(path, `possible secret: ${pattern}`);
143
+ }
144
+ }
145
+ if (withheld.size === 0)
146
+ return;
147
+ gitOutput(['reset', '-q', baseSha, '--pathspec-from-file=-', '--pathspec-file-nul'], gitRoot, deadline, { env: { ...indexEnv, GIT_LITERAL_PATHSPECS: '1' }, input: `${[...withheld.keys()].join('\0')}\0` });
148
+ for (const [path, reason] of withheld) {
149
+ process.stderr.write(`haystack: withheld ${JSON.stringify(path)} from the precompute upload (${reason}).\n`);
150
+ }
151
+ }
152
+ function deriveWorktreeTreeSha(gitRoot, baseSha, deadline) {
114
153
  const scratchDirectory = mkdtempSync(join(tmpdir(), 'haystack-precompute-index-'));
115
154
  const temporaryIndex = join(scratchDirectory, 'index');
116
155
  try {
@@ -128,6 +167,7 @@ function deriveWorktreeTreeSha(gitRoot, deadline) {
128
167
  gitOutput(['read-tree', 'HEAD'], gitRoot, deadline, { env: indexEnv });
129
168
  }
130
169
  gitOutput(['add', '-A'], gitRoot, deadline, { env: indexEnv });
170
+ withholdSecretBearingChanges(gitRoot, baseSha, indexEnv, deadline);
131
171
  const treeSha = gitOutput(['write-tree'], gitRoot, deadline, { env: indexEnv });
132
172
  if (!isCommitSha(treeSha)) {
133
173
  throw new PrecomputeFailure('Could not derive the working-tree snapshot.');
@@ -221,7 +261,7 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
221
261
  // make an older stop look newer than a later one.
222
262
  const observedAt = Date.now();
223
263
  const baseSha = deriveBaseSha(gitRoot, deadline);
224
- const treeSha = deriveWorktreeTreeSha(gitRoot, deadline);
264
+ const treeSha = deriveWorktreeTreeSha(gitRoot, baseSha, deadline);
225
265
  const workCommit = deriveWorkCommit(gitRoot, baseSha, treeSha, deadline);
226
266
  const patchGzip = await buildCompressedPatch(gitRoot, baseSha, workCommit, deadline);
227
267
  const cacheKey = [
@@ -231,6 +271,7 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
231
271
  'slim',
232
272
  slimInputHash(),
233
273
  ].join('-');
274
+ const combinationSearch = captureCombinationSearchHook(gitRoot, `${owner}/${repository}`, baseSha, workCommit, deadline);
234
275
  return {
235
276
  treeSha,
236
277
  request: {
@@ -242,6 +283,7 @@ export async function derivePrecomputeRequest(gitRoot, owner, repository, deadli
242
283
  cacheKey,
243
284
  patchGzBase64: patchGzip.toString('base64'),
244
285
  patchSha256: createHash('sha256').update(patchGzip).digest('hex'),
286
+ ...(combinationSearch ? { combinationSearch } : {}),
245
287
  },
246
288
  };
247
289
  }
@@ -249,15 +291,16 @@ function precomputeResult(response) {
249
291
  if (!response || typeof response !== 'object' || Array.isArray(response)) {
250
292
  throw new PrecomputeFailure('The precompute service returned an invalid response.');
251
293
  }
252
- const { status, runId, walker: rawWalker } = response;
294
+ const { status, runId, walker: rawWalker, search: rawSearch } = response;
253
295
  const walker = rawWalker === undefined ? undefined : readWalkerStatus(rawWalker);
296
+ const search = rawSearch === undefined ? undefined : readCombinationSearchHookStatus(rawSearch);
254
297
  if (status === 'queued' && typeof runId === 'string' && isRunId(runId)) {
255
- return { outcome: 'enqueued', runId, ...(walker === undefined ? {} : { walker }) };
298
+ return { outcome: 'enqueued', runId, ...(walker === undefined ? {} : { walker }), ...(search === undefined ? {} : { search }) };
256
299
  }
257
300
  if (status === 'cached')
258
- return { outcome: 'cached', ...(walker === undefined ? {} : { walker }) };
301
+ return { outcome: 'cached', ...(walker === undefined ? {} : { walker }), ...(search === undefined ? {} : { search }) };
259
302
  if (status === 'existing')
260
- return { outcome: 'existing', ...(walker === undefined ? {} : { walker }) };
303
+ return { outcome: 'existing', ...(walker === undefined ? {} : { walker }), ...(search === undefined ? {} : { search }) };
261
304
  throw new PrecomputeFailure('The precompute service returned an invalid response.');
262
305
  }
263
306
  /** One authenticated submission of the already captured request. No source reads or retries. */
@@ -325,6 +368,12 @@ function normalModeFailure(error) {
325
368
  return new PrecomputeFailure('Authentication is required. Run `haystack login` first.');
326
369
  }
327
370
  if (error instanceof HaystackApiError) {
371
+ // Entitlement denials are not authentication problems: classifyHttpError
372
+ // already turned the server body into its one-line message, so show it.
373
+ if (error.status === 403
374
+ && (error.code === REPO_NOT_ENTITLED_CODE || error.code === SUBSCRIPTION_REQUIRED_CODE)) {
375
+ return new PrecomputeFailure(error.message);
376
+ }
328
377
  if (error.status === 401 || error.status === 403) {
329
378
  return new PrecomputeFailure('Authentication was rejected. Run `haystack login` and try again.');
330
379
  }
@@ -340,7 +389,7 @@ function normalModeFailure(error) {
340
389
  }
341
390
  return new PrecomputeFailure('The precompute request failed.');
342
391
  }
343
- function printResult(outcome, runId, walker) {
392
+ function printResult(outcome, runId, walker, search) {
344
393
  if (outcome === 'enqueued') {
345
394
  console.log(chalk.green(`Precompute request queued${runId ? ` (${runId})` : ''}.`));
346
395
  }
@@ -360,6 +409,10 @@ function printResult(outcome, runId, walker) {
360
409
  : 'Walker unavailable: the enqueue request failed.');
361
410
  else
362
411
  console.log('Walker status was not reported by the service.');
412
+ if (search?.status === 'queued' || search?.status === 'existing')
413
+ console.log(`Browser search ${search.status} (${search.runId}).`);
414
+ else if (search?.status === 'unavailable' && search.reason !== 'not-configured')
415
+ console.log(`Browser search unavailable: ${search.reason}.`);
363
416
  }
364
417
  export async function verifyPrecomputeCommand(options) {
365
418
  const startedAt = Date.now();
@@ -421,7 +474,7 @@ export async function verifyPrecomputeCommand(options) {
421
474
  }
422
475
  }
423
476
  if (!options.hook)
424
- printResult(outcome, result.runId, result.walker);
477
+ printResult(outcome, result.runId, result.walker, result.search);
425
478
  }
426
479
  catch (error) {
427
480
  if (!options.hook)