@haystackeditor/cli 0.15.30 → 0.16.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 (135) hide show
  1. package/README.md +111 -60
  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 +608 -0
  8. package/dist/commands/case-batch.js +713 -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/design-verify.js +27 -1
  12. package/dist/commands/dismiss.js +2 -1
  13. package/dist/commands/hooks.js +66 -7
  14. package/dist/commands/install-session-hooks.js +131 -44
  15. package/dist/commands/policy.js +32 -45
  16. package/dist/commands/precompute-delivery.js +6 -1
  17. package/dist/commands/scaffold-provisional-universe.js +8 -10
  18. package/dist/commands/setup.js +32 -10
  19. package/dist/commands/submit.js +7 -2
  20. package/dist/commands/telemetry.js +17 -2
  21. package/dist/commands/triage.js +2 -1
  22. package/dist/commands/verify-explore.js +1 -0
  23. package/dist/commands/verify-history.js +152 -0
  24. package/dist/commands/verify-hosted-mcp.js +3 -12
  25. package/dist/commands/verify-hosted-reproducibility.js +49 -547
  26. package/dist/commands/verify-hosted.js +180 -163
  27. package/dist/commands/verify-precompute.js +57 -10
  28. package/dist/commands/verify.js +51 -139
  29. package/dist/index.js +264 -348
  30. package/dist/lazy.js +8 -0
  31. package/dist/schema.js +1 -0
  32. package/dist/tools/detect.js +3 -24
  33. package/dist/triage/prompts.js +46 -45
  34. package/dist/triage/runner.js +119 -33
  35. package/dist/types/verify-history.js +1 -0
  36. package/dist/types.js +3 -3
  37. package/dist/utils/auth.js +14 -2
  38. package/dist/utils/design-verifier-api.js +23 -0
  39. package/dist/utils/design-verifier-history.js +79 -0
  40. package/dist/utils/git.js +60 -29
  41. package/dist/utils/github-api.js +14 -1
  42. package/dist/utils/haystack-api.js +26 -7
  43. package/dist/utils/hooks.js +43 -6
  44. package/dist/utils/prompter.js +25 -10
  45. package/dist/utils/safe-write.js +31 -0
  46. package/dist/utils/secret-paths.js +115 -0
  47. package/dist/utils/secrets.js +0 -1
  48. package/dist/utils/telemetry.js +34 -14
  49. package/dist/utils/update-check.js +151 -0
  50. package/package.json +18 -14
  51. package/schemas/case-batch.v1.json +184 -0
  52. package/schemas/cloud-verifier.v1.json +55 -100
  53. package/dist/commands/ask.d.ts +0 -14
  54. package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
  55. package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
  56. package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
  57. package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
  58. package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
  59. package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
  60. package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
  61. package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
  62. package/dist/commands/cloud-verifier-universe.d.ts +0 -31
  63. package/dist/commands/config.d.ts +0 -46
  64. package/dist/commands/design-verify.d.ts +0 -31
  65. package/dist/commands/dismiss.d.ts +0 -29
  66. package/dist/commands/hooks.d.ts +0 -13
  67. package/dist/commands/inbox.d.ts +0 -65
  68. package/dist/commands/init.d.ts +0 -10
  69. package/dist/commands/install-session-hooks.d.ts +0 -17
  70. package/dist/commands/login.d.ts +0 -8
  71. package/dist/commands/mcp.d.ts +0 -1
  72. package/dist/commands/policy.d.ts +0 -31
  73. package/dist/commands/pr-status.d.ts +0 -144
  74. package/dist/commands/pr.d.ts +0 -40
  75. package/dist/commands/precompute-delivery-contract.d.ts +0 -68
  76. package/dist/commands/precompute-delivery.d.ts +0 -20
  77. package/dist/commands/prepare-universe-review.d.ts +0 -115
  78. package/dist/commands/production-source-deny-policy.d.ts +0 -15
  79. package/dist/commands/request-review.d.ts +0 -26
  80. package/dist/commands/review.d.ts +0 -25
  81. package/dist/commands/rules.d.ts +0 -4
  82. package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
  83. package/dist/commands/schema-cmd.d.ts +0 -2
  84. package/dist/commands/setup.d.ts +0 -28
  85. package/dist/commands/skills.d.ts +0 -8
  86. package/dist/commands/status.d.ts +0 -4
  87. package/dist/commands/submit.d.ts +0 -30
  88. package/dist/commands/system-map.d.ts +0 -42
  89. package/dist/commands/telemetry.d.ts +0 -53
  90. package/dist/commands/tokens.d.ts +0 -14
  91. package/dist/commands/triage.d.ts +0 -35
  92. package/dist/commands/verify-core.d.ts +0 -449
  93. package/dist/commands/verify-core.js +0 -789
  94. package/dist/commands/verify-explore.d.ts +0 -14
  95. package/dist/commands/verify-hosted-mcp.d.ts +0 -12
  96. package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
  97. package/dist/commands/verify-hosted.d.ts +0 -92
  98. package/dist/commands/verify-mcp.d.ts +0 -8
  99. package/dist/commands/verify-mcp.js +0 -517
  100. package/dist/commands/verify-ops.d.ts +0 -158
  101. package/dist/commands/verify-ops.js +0 -1148
  102. package/dist/commands/verify-precompute.d.ts +0 -30
  103. package/dist/commands/verify-reproducibility.d.ts +0 -85
  104. package/dist/commands/verify-reproducibility.js +0 -494
  105. package/dist/commands/verify-reseal.d.ts +0 -9
  106. package/dist/commands/verify-reseal.js +0 -148
  107. package/dist/commands/verify-sandboxes.d.ts +0 -95
  108. package/dist/commands/verify-sandboxes.js +0 -352
  109. package/dist/commands/verify.d.ts +0 -28
  110. package/dist/commands/webhooks.d.ts +0 -30
  111. package/dist/index.d.ts +0 -22
  112. package/dist/schema.d.ts +0 -28
  113. package/dist/states.d.ts +0 -29
  114. package/dist/tools/detect.d.ts +0 -50
  115. package/dist/triage/prompts.d.ts +0 -24
  116. package/dist/triage/runner.d.ts +0 -34
  117. package/dist/triage/types.d.ts +0 -42
  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 -200
  124. package/dist/utils/design-verifier-result.d.ts +0 -74
  125. package/dist/utils/detect.d.ts +0 -43
  126. package/dist/utils/git.d.ts +0 -135
  127. package/dist/utils/github-api.d.ts +0 -104
  128. package/dist/utils/haystack-api.d.ts +0 -37
  129. package/dist/utils/hooks.d.ts +0 -12
  130. package/dist/utils/pending-state.d.ts +0 -40
  131. package/dist/utils/pr-ref.d.ts +0 -27
  132. package/dist/utils/prompter.d.ts +0 -85
  133. package/dist/utils/secrets.d.ts +0 -47
  134. package/dist/utils/telemetry.d.ts +0 -19
  135. /package/dist/commands/{precompute-delivery-worker.d.ts → combination-search-hook-contract.js} +0 -0
@@ -76,95 +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
- requireString(provenance, 'intent', /^(user|inferred)$/);
136
- if (status === 'complete' && fleetResult.lifecycle !== 'complete') {
137
- throw new Error('Hosted fleet lifecycle disagrees with its completed job.');
138
- }
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.');
139
119
  }
140
- if (status === 'complete' && !fleetResult) {
141
- 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');
142
125
  }
143
- }
144
- else if (status === 'complete') {
145
- // Two terminal shapes exist: the Workflow receipt envelope (`output`)
146
- // and the runner-queue result envelope (`result`). Exactly one must
147
- // validate.
148
- if (result) {
149
- if (!repository || !baseCommit || !headCommit || !createdAt) {
150
- throw new Error('Hosted verifier returned a complete run without a matching terminal receipt.');
151
- }
126
+ if (fleetResult.savedChecks !== undefined) {
127
+ parseHostedSavedChecks(fleetResult.savedChecks, runId, headCommit);
152
128
  }
153
- else if (!repository
154
- || !baseCommit
155
- || !headCommit
156
- || !createdAt
157
- || !completedAt
158
- || !output
159
- || output.runId !== runId
160
- || output.repository !== repository
161
- || output.baseCommit !== baseCommit
162
- || output.headCommit !== headCommit
163
- || output.sourceOfflineReady !== true
164
- || output.sandboxDeleted !== true) {
165
- 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.');
166
132
  }
167
133
  }
134
+ if (status === 'complete' && !fleetResult) {
135
+ throw new Error('Hosted fleet run completed without a result receipt.');
136
+ }
168
137
  return {
169
138
  runId,
170
139
  status,
@@ -173,37 +142,13 @@ export function parseHostedRunState(value, expectedRunId) {
173
142
  headCommit,
174
143
  createdAt,
175
144
  completedAt,
176
- output,
177
- result,
178
- ...(fleet ? { provider: 'haystack', version: FLEET_PROJECTION_VERSION, progress, fleetResult } : {}),
145
+ provider: 'haystack',
146
+ version: FLEET_PROJECTION_VERSION,
147
+ progress,
148
+ fleetResult,
179
149
  error: optionalString(response, 'error'),
180
150
  };
181
151
  }
182
- function parseHostedRunnerResult(value) {
183
- const record = requireRecord(value, 'Hosted verifier runner result');
184
- const regressionsValue = record.regressions;
185
- if (typeof record.verdict !== 'string'
186
- || typeof record.summary !== 'string'
187
- || !Number.isInteger(record.casesTotal)
188
- || !Number.isInteger(record.casesFailed)
189
- || typeof record.outputLocation !== 'string'
190
- || typeof record.wallSeconds !== 'number'
191
- || !Array.isArray(regressionsValue)
192
- || !regressionsValue.every(item => !!item && typeof item === 'object' && !Array.isArray(item)
193
- && typeof item.caseId === 'string'
194
- && typeof item.title === 'string')) {
195
- throw new Error('Hosted verifier runner result is malformed.');
196
- }
197
- return {
198
- verdict: record.verdict,
199
- summary: record.summary,
200
- casesTotal: record.casesTotal,
201
- casesFailed: record.casesFailed,
202
- regressions: regressionsValue,
203
- outputLocation: record.outputLocation,
204
- wallSeconds: record.wallSeconds,
205
- };
206
- }
207
152
  export function parseHostedRepository(value) {
208
153
  const parts = value.split('/');
209
154
  if (parts.length !== 2
@@ -216,13 +161,95 @@ export function parseHostedRepository(value) {
216
161
  fullName: value,
217
162
  };
218
163
  }
219
- function requireCommit(value, flag) {
220
- if (typeof value !== 'string' || !COMMIT_SHA.test(value)) {
221
- throw new Error(`${flag} must be an exact 40-character lowercase commit SHA.`);
164
+ /** The service validates the shared full contract. Check every additional
165
+ * field used by this published CLI before displaying or accepting a clean run. */
166
+ function parseHostedSavedChecks(value, runId, headSha) {
167
+ const saved = requireRecord(value, 'Hosted saved checks');
168
+ if (saved.version !== 'dv-fleet-saved-check-result-v1'
169
+ || !Array.isArray(saved.descriptors) || !Array.isArray(saved.outcomes)
170
+ || !Array.isArray(saved.unresolvedFindings) || !Array.isArray(saved.conflicts)) {
171
+ throw new Error('Hosted saved checks have an invalid version or records.');
172
+ }
173
+ const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
174
+ requireString(completeness, 'status', /^(complete|incomplete)$/);
175
+ if (completeness.status === 'incomplete' && (!Array.isArray(completeness.reasons) || completeness.reasons.length === 0)) {
176
+ throw new Error('Hosted saved check history has no explanation for missing evidence.');
177
+ }
178
+ const ref = (record) => {
179
+ requireString(record, 'checkId', /^[0-9a-f]{64}$/);
180
+ requireString(record, 'planRevisionId', /^[0-9a-f]{64}$/);
181
+ return `${record.checkId}:${record.planRevisionId}`;
182
+ };
183
+ const descriptors = new Map();
184
+ for (const value of saved.descriptors) {
185
+ const descriptor = requireRecord(value, 'Hosted saved check descriptor');
186
+ const key = ref(descriptor);
187
+ if (descriptor.version !== 'dv-fleet-saved-check-v1' || descriptors.has(key)) {
188
+ throw new Error('Hosted saved check descriptor identity is invalid or duplicated.');
189
+ }
190
+ requireString(descriptor, 'captureStatus', /^(captured|unavailable)$/);
191
+ descriptors.set(key, descriptor);
192
+ }
193
+ const outcomes = new Map();
194
+ for (const value of saved.outcomes) {
195
+ const outcome = requireRecord(value, 'Hosted saved check outcome');
196
+ const key = ref(outcome);
197
+ requireString(outcome, 'status', /^(passed|reproduced|blocked|inconclusive)$/);
198
+ if (outcome.version !== 'dv-fleet-saved-check-outcome-v1'
199
+ || (!descriptors.has(key) && completeness.status !== 'incomplete') || outcomes.has(key)
200
+ || outcome.publicRunId !== runId || outcome.headSha !== headSha
201
+ || (outcome.currentCaseId !== null && typeof outcome.currentCaseId !== 'string')
202
+ || (outcome.blockedCode !== null && typeof outcome.blockedCode !== 'string')) {
203
+ throw new Error('Hosted saved check outcome does not match its current run or original check.');
204
+ }
205
+ if ((outcome.status === 'blocked') !== (outcome.blockedCode !== null)
206
+ || (outcome.status === 'passed' && (descriptors.get(key)?.captureStatus !== 'captured'
207
+ || outcome.currentCaseId === null || !outcome.receipt || completeness.status !== 'complete'))) {
208
+ throw new Error('Hosted saved check outcome claims a pass without original execution evidence.');
209
+ }
210
+ outcomes.set(key, outcome);
211
+ }
212
+ const findings = new Set();
213
+ for (const value of saved.unresolvedFindings) {
214
+ const finding = requireRecord(value, 'Hosted unresolved finding');
215
+ const key = ref(finding);
216
+ requireString(finding, 'findingId');
217
+ const source = requireRecord(finding.firstObserved, 'Hosted original finding evidence');
218
+ requireString(source, 'publicRunId', RUN_ID);
219
+ requireString(source, 'caseId');
220
+ requireString(source, 'headSha', COMMIT_SHA);
221
+ if (finding.version !== 'dv-fleet-unresolved-finding-v1'
222
+ || (!descriptors.has(key) && completeness.status !== 'incomplete')
223
+ || findings.has(finding.findingId) || !Array.isArray(source.evidence) || source.evidence.length === 0
224
+ || outcomes.get(key)?.status === 'passed') {
225
+ throw new Error('Hosted unresolved finding has invalid original evidence or check identity.');
226
+ }
227
+ findings.add(finding.findingId);
228
+ if (finding.lastOutcome !== null) {
229
+ const last = requireRecord(finding.lastOutcome, 'Hosted unresolved finding outcome');
230
+ if (ref(last) !== key || last.status === 'passed')
231
+ throw new Error('Hosted unresolved finding lost its exact original check.');
232
+ requireString(last, 'status', /^(reproduced|blocked|inconclusive)$/);
233
+ if (last.blockedCode !== null)
234
+ requireString(last, 'blockedCode');
235
+ }
222
236
  }
223
- return value;
237
+ for (const value of saved.conflicts) {
238
+ const conflict = requireRecord(value, 'Hosted saved check conflict');
239
+ requireString(conflict, 'checkId', /^[0-9a-f]{64}$/);
240
+ if (!Array.isArray(conflict.planRevisionIds) || conflict.planRevisionIds.length < 2
241
+ || conflict.planRevisionIds.some(id => typeof id !== 'string' || !/^[0-9a-f]{64}$/.test(id))) {
242
+ throw new Error('Hosted saved check conflict is invalid.');
243
+ }
244
+ }
245
+ return saved;
246
+ }
247
+ function savedCheckDisplay(value) {
248
+ // eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
249
+ return String(value).replace(/[\u0000-\u001f\u007f-\u009f]/g, '');
224
250
  }
225
251
  function requireRevision(value, flag) {
252
+ // eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
226
253
  if (typeof value !== 'string' || value.length === 0 || value.length > 1_024 || /[\u0000-\u0020\u007f]/.test(value)) {
227
254
  throw new Error(`${flag} must be a non-empty Git revision of at most 1024 characters without whitespace or control characters.`);
228
255
  }
@@ -276,13 +303,9 @@ export function readHostedIntentFile(intentFile) {
276
303
  intended_outcomes: record.intended_outcomes,
277
304
  };
278
305
  }
279
- export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, replayPlanFrom, fleetRequest) {
306
+ export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, intent) {
280
307
  const digest = createHash('sha256')
281
- .update(JSON.stringify(fleetRequest
282
- ? [fleetRequest.provider, repository, baseCommit, headCommit, fleetRequest.intent ?? null]
283
- : replayPlanFrom
284
- ? [repository, baseCommit, headCommit, replayPlanFrom]
285
- : [repository, baseCommit, headCommit]))
308
+ .update(JSON.stringify(['haystack', repository, baseCommit, headCommit, intent ?? null]))
286
309
  .digest('hex');
287
310
  return `cli:${digest}`;
288
311
  }
@@ -309,7 +332,7 @@ export function hostedPollingOptions(interval, timeout, onStatus) {
309
332
  };
310
333
  }
311
334
  export async function startHostedVerification(repositoryArg, options, token) {
312
- const { repository, launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
335
+ const { launchBody, idempotencyKey } = prepareHostedLaunch(repositoryArg, options);
313
336
  const path = '/api/agent/cloud-verifier/runs';
314
337
  const response = await fetch(haystackApiUrl(path), {
315
338
  method: 'POST',
@@ -335,49 +358,27 @@ export async function startHostedVerification(repositoryArg, options, token) {
335
358
  throw error;
336
359
  }
337
360
  const state = parseHostedRunState(await response.json());
338
- if (options.provider === 'haystack' && state.provider !== 'haystack') {
339
- throw new Error('Hosted verifier did not acknowledge the fleet-pinned request.');
340
- }
341
- return {
342
- state: {
343
- ...state,
344
- repository: state.repository ?? repository.fullName,
345
- baseCommit: state.baseCommit ?? launchBody.baseCommit,
346
- headCommit: state.headCommit ?? launchBody.headCommit,
347
- },
348
- idempotencyKey,
349
- };
361
+ return { state, idempotencyKey };
350
362
  }
351
363
  function prepareHostedLaunch(repositoryArg, options) {
352
364
  const repository = parseHostedRepository(repositoryArg);
353
- const fleet = options.provider === 'haystack';
354
- const validateRevision = fleet ? requireRevision : requireCommit;
355
- const baseCommit = validateRevision(options.base, '--base');
356
- const headCommit = validateRevision(options.head, '--head');
357
- const intent = fleet && options.intentFile === undefined ? undefined : readHostedIntentFile(options.intentFile);
358
- 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))) {
359
370
  throw new Error('Fleet --intent-file fields must be single-line text without control characters.');
360
371
  }
361
- const replayPlanFrom = options.replayPlanFrom;
362
- if (fleet && replayPlanFrom !== undefined) {
363
- throw new Error('--replay-plan-from is available through the explicit hosted start alias.');
364
- }
365
- if (replayPlanFrom !== undefined && !RUN_ID.test(replayPlanFrom)) {
366
- throw new Error('--replay-plan-from must match cv_ followed by 48 lowercase hex characters.');
367
- }
368
372
  const idempotencyKey = requireIdempotencyKey(options.idempotencyKey
369
- ?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, replayPlanFrom, fleet ? { provider: 'haystack', intent } : undefined));
373
+ ?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, intent));
370
374
  const launchBody = {
371
- ...(fleet ? { provider: 'haystack' } : {}),
375
+ provider: 'haystack',
372
376
  owner: repository.owner,
373
377
  repository: repository.repository,
374
378
  baseCommit,
375
379
  headCommit,
376
380
  idempotencyKey,
377
381
  ...(intent ? { intent } : {}),
378
- ...(replayPlanFrom
379
- ? { replayPlanFromRunId: replayPlanFrom }
380
- : {}),
381
382
  };
382
383
  return { repository, launchBody, idempotencyKey };
383
384
  }
@@ -437,9 +438,8 @@ export function buildHostedJson(operation, state, extras = {}) {
437
438
  : {}),
438
439
  ...(extras.nextCommand ? { next_command: extras.nextCommand } : {}),
439
440
  ...(extras.timedOut !== undefined ? { timed_out: extras.timedOut } : {}),
440
- ...(state.output ? { output: state.output } : {}),
441
- ...(state.result ? { result: state.result } : {}),
442
- ...(state.provider ? { provider: state.provider, version: state.version } : {}),
441
+ provider: state.provider,
442
+ version: state.version,
443
443
  ...(state.progress ? { progress: state.progress } : {}),
444
444
  ...(state.fleetResult ? { result: state.fleetResult } : {}),
445
445
  ...(state.error ? { error: state.error } : {}),
@@ -461,13 +461,11 @@ function printHostedState(operation, state, options) {
461
461
  }), null, 2)}\n`);
462
462
  return;
463
463
  }
464
- const icon = state.provider === 'haystack' && state.status === 'complete'
464
+ const icon = state.status === 'complete'
465
465
  ? chalk.cyan('•')
466
- : state.status === 'complete'
467
- ? chalk.green('✓')
468
- : TERMINAL_STATUSES.has(state.status)
469
- ? chalk.red('✗')
470
- : chalk.cyan('•');
466
+ : TERMINAL_STATUSES.has(state.status)
467
+ ? chalk.red('✗')
468
+ : chalk.cyan('•');
471
469
  console.log(`${icon} Hosted verification ${chalk.bold(state.runId)}: ${state.status}`);
472
470
  if (state.repository) {
473
471
  console.log(` ${chalk.dim('Repository:')} ${state.repository}`);
@@ -475,15 +473,6 @@ function printHostedState(operation, state, options) {
475
473
  if (state.baseCommit && state.headCommit) {
476
474
  console.log(` ${chalk.dim('Commits:')} ${state.baseCommit.slice(0, 12)} → ${state.headCommit.slice(0, 12)}`);
477
475
  }
478
- if (state.result) {
479
- const passed = state.result.casesTotal - state.result.casesFailed;
480
- console.log(` ${chalk.dim('Verdict:')} ${state.result.verdict} `
481
- + `(${passed}/${state.result.casesTotal} cases clean, ${Math.round(state.result.wallSeconds)}s)`);
482
- for (const regression of state.result.regressions) {
483
- console.log(` ${chalk.red('✗')} ${regression.caseId}: ${regression.title}`);
484
- }
485
- console.log(` ${chalk.dim('Artifacts:')} ${state.result.outputLocation}`);
486
- }
487
476
  if (state.progress) {
488
477
  console.log(` ${chalk.dim('Progress:')} ${state.progress.stage} (${state.progress.status})`);
489
478
  }
@@ -499,6 +488,26 @@ function printHostedState(operation, state, options) {
499
488
  const finding = requireRecord(value, 'Hosted fleet finding');
500
489
  console.log(` ${chalk.dim('Finding:')} ${finding.findingId} / ${finding.caseId}: ${finding.judgment}`);
501
490
  }
491
+ if (result.savedChecks !== undefined) {
492
+ const saved = requireRecord(result.savedChecks, 'Hosted saved checks');
493
+ const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
494
+ const unresolved = saved.unresolvedFindings;
495
+ console.log(` ${chalk.dim('Saved checks:')} ${completeness.status}; ${saved.conflicts.length} conflicts; ${unresolved.length} unresolved bugs`);
496
+ for (const value of unresolved) {
497
+ const finding = requireRecord(value, 'Hosted unresolved finding');
498
+ const source = requireRecord(finding.firstObserved, 'Hosted original finding evidence');
499
+ const last = finding.lastOutcome === null ? null : requireRecord(finding.lastOutcome, 'Hosted unresolved finding outcome');
500
+ console.log(` ${chalk.yellow('Unresolved:')} ${savedCheckDisplay(finding.findingId)} / ${savedCheckDisplay(source.caseId)}`);
501
+ console.log(` First reproduced at ${String(source.headSha).slice(0, 12)}; latest check: ${savedCheckDisplay(last?.status ?? 'not executed')}${last?.blockedCode ? ` (${savedCheckDisplay(last.blockedCode)})` : ''}`);
502
+ }
503
+ if (unresolved.length)
504
+ console.log(' Earlier bug evidence remains open until the exact saved check passes.');
505
+ if (completeness.status === 'incomplete')
506
+ console.log(' Saved check history is incomplete; this run cannot confirm earlier bugs are fixed.');
507
+ }
508
+ else {
509
+ console.log(` ${chalk.dim('Saved checks:')} unavailable for this run`);
510
+ }
502
511
  }
503
512
  if (state.error)
504
513
  console.log(` ${chalk.dim('Error:')} ${state.error}`);
@@ -516,6 +525,16 @@ function markNonSuccess(state, timedOut = false) {
516
525
  process.exitCode = 2;
517
526
  return;
518
527
  }
528
+ if (result.savedChecks !== undefined) {
529
+ const saved = requireRecord(result.savedChecks, 'Hosted saved checks');
530
+ const completeness = requireRecord(saved.completeness, 'Hosted saved check completeness');
531
+ if (completeness.status !== 'complete' || saved.conflicts.length > 0) {
532
+ process.exitCode = 2;
533
+ return;
534
+ }
535
+ if (saved.unresolvedFindings.length > 0)
536
+ process.exitCode = 1;
537
+ }
519
538
  if (result.findings.some(value => requireRecord(value, 'Hosted fleet finding').judgment === 'regression')) {
520
539
  process.exitCode = 1;
521
540
  }
@@ -538,9 +557,7 @@ export async function verifyHostedStartCommand(repositoryArg, options) {
538
557
  repo: repository.repository,
539
558
  });
540
559
  if (!options.json) {
541
- console.error(chalk.dim(options.provider === 'haystack'
542
- ? 'Submitting repository revisions to the hosted fleet verifier…'
543
- : 'Submitting the exact commit pair to the hosted verifier…'));
560
+ console.error(chalk.dim('Submitting repository revisions to the hosted fleet verifier…'));
544
561
  }
545
562
  const started = await startHostedVerification(repositoryArg, options, auth.token);
546
563
  const selectedAccount = auth.token.startsWith('hsk_live_')
@@ -9,7 +9,9 @@ import { NotLoggedInError, resolveAuthContext } from '../utils/auth.js';
9
9
  import { findGitRoot, parseRemoteUrl, resolveDiffBaseRef, } from '../utils/git.js';
10
10
  import { HaystackApiError, haystackJson } 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. */
@@ -340,7 +383,7 @@ function normalModeFailure(error) {
340
383
  }
341
384
  return new PrecomputeFailure('The precompute request failed.');
342
385
  }
343
- function printResult(outcome, runId, walker) {
386
+ function printResult(outcome, runId, walker, search) {
344
387
  if (outcome === 'enqueued') {
345
388
  console.log(chalk.green(`Precompute request queued${runId ? ` (${runId})` : ''}.`));
346
389
  }
@@ -360,6 +403,10 @@ function printResult(outcome, runId, walker) {
360
403
  : 'Walker unavailable: the enqueue request failed.');
361
404
  else
362
405
  console.log('Walker status was not reported by the service.');
406
+ if (search?.status === 'queued' || search?.status === 'existing')
407
+ console.log(`Browser search ${search.status} (${search.runId}).`);
408
+ else if (search?.status === 'unavailable' && search.reason !== 'not-configured')
409
+ console.log(`Browser search unavailable: ${search.reason}.`);
363
410
  }
364
411
  export async function verifyPrecomputeCommand(options) {
365
412
  const startedAt = Date.now();
@@ -421,7 +468,7 @@ export async function verifyPrecomputeCommand(options) {
421
468
  }
422
469
  }
423
470
  if (!options.hook)
424
- printResult(outcome, result.runId, result.walker);
471
+ printResult(outcome, result.runId, result.walker, result.search);
425
472
  }
426
473
  catch (error) {
427
474
  if (!options.hook)