@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.
- package/README.md +141 -61
- package/dist/assets/hooks/scripts/commit-msg.sh +3 -0
- package/dist/assets/hooks/scripts/post-commit.sh +3 -0
- package/dist/assets/hooks/scripts/pre-commit.sh +11 -6
- package/dist/assets/hooks/scripts/pre-push.sh +3 -0
- package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
- package/dist/commands/case-batch-contract.js +694 -0
- package/dist/commands/case-batch.js +1011 -0
- package/dist/commands/cloud-verifier-identity-census.js +5 -2
- package/dist/commands/combination-search-hook.js +139 -0
- package/dist/commands/dismiss.js +2 -1
- package/dist/commands/hooks.js +66 -7
- package/dist/commands/install-session-hooks.js +131 -44
- package/dist/commands/policy.js +32 -45
- package/dist/commands/precompute-delivery.js +6 -1
- package/dist/commands/scaffold-provisional-universe.js +8 -10
- package/dist/commands/setup.js +32 -10
- package/dist/commands/submit.js +39 -71
- package/dist/commands/telemetry.js +17 -2
- package/dist/commands/triage.js +2 -1
- package/dist/commands/verify-explore.js +1 -0
- package/dist/commands/verify-hosted-mcp.js +3 -12
- package/dist/commands/verify-hosted-reproducibility.js +49 -547
- package/dist/commands/verify-hosted.js +66 -168
- package/dist/commands/verify-precompute.js +64 -11
- package/dist/commands/verify.js +51 -139
- package/dist/index.js +290 -366
- package/dist/lazy.js +8 -0
- package/dist/schema.js +2 -1
- package/dist/tools/detect.js +3 -24
- package/dist/triage/astra.js +199 -0
- package/dist/triage/prompts.js +145 -179
- package/dist/triage/runner.js +104 -300
- package/dist/triage/types.js +2 -2
- package/dist/types.js +2 -6
- package/dist/utils/auth.js +14 -2
- package/dist/utils/git.js +60 -29
- package/dist/utils/github-api.js +14 -1
- package/dist/utils/haystack-api.js +38 -7
- package/dist/utils/hooks.js +43 -6
- package/dist/utils/prompter.js +25 -10
- package/dist/utils/safe-write.js +31 -0
- package/dist/utils/secret-paths.js +115 -0
- package/dist/utils/secrets.js +0 -1
- package/dist/utils/telemetry.js +34 -14
- package/dist/utils/update-check.js +151 -0
- package/package.json +19 -14
- package/schemas/case-batch.v1.json +235 -0
- package/schemas/cloud-verifier.v1.json +55 -100
- package/schemas/submit.v1.json +4 -2
- package/dist/commands/ask.d.ts +0 -14
- package/dist/commands/cloud-verifier-behaviors.d.ts +0 -27
- package/dist/commands/cloud-verifier-data-store-census.d.ts +0 -47
- package/dist/commands/cloud-verifier-data-store-drift.d.ts +0 -42
- package/dist/commands/cloud-verifier-identity-census.d.ts +0 -88
- package/dist/commands/cloud-verifier-materialization.d.ts +0 -16
- package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +0 -29
- package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +0 -27
- package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +0 -51
- package/dist/commands/cloud-verifier-universe.d.ts +0 -31
- package/dist/commands/config.d.ts +0 -46
- package/dist/commands/design-verify.d.ts +0 -33
- package/dist/commands/dismiss.d.ts +0 -29
- package/dist/commands/hooks.d.ts +0 -13
- package/dist/commands/inbox.d.ts +0 -65
- package/dist/commands/init.d.ts +0 -10
- package/dist/commands/install-session-hooks.d.ts +0 -17
- package/dist/commands/login.d.ts +0 -8
- package/dist/commands/mcp.d.ts +0 -1
- package/dist/commands/policy.d.ts +0 -31
- package/dist/commands/pr-status.d.ts +0 -144
- package/dist/commands/pr.d.ts +0 -40
- package/dist/commands/precompute-delivery-contract.d.ts +0 -68
- package/dist/commands/precompute-delivery.d.ts +0 -20
- package/dist/commands/prepare-universe-review.d.ts +0 -115
- package/dist/commands/production-source-deny-policy.d.ts +0 -15
- package/dist/commands/request-review.d.ts +0 -26
- package/dist/commands/review.d.ts +0 -25
- package/dist/commands/rules.d.ts +0 -4
- package/dist/commands/scaffold-provisional-universe.d.ts +0 -468
- package/dist/commands/schema-cmd.d.ts +0 -2
- package/dist/commands/setup.d.ts +0 -28
- package/dist/commands/skills.d.ts +0 -8
- package/dist/commands/status.d.ts +0 -4
- package/dist/commands/submit.d.ts +0 -30
- package/dist/commands/system-map.d.ts +0 -42
- package/dist/commands/telemetry.d.ts +0 -53
- package/dist/commands/tokens.d.ts +0 -14
- package/dist/commands/triage.d.ts +0 -35
- package/dist/commands/verify-core.d.ts +0 -449
- package/dist/commands/verify-core.js +0 -789
- package/dist/commands/verify-explore.d.ts +0 -14
- package/dist/commands/verify-history.d.ts +0 -14
- package/dist/commands/verify-hosted-mcp.d.ts +0 -12
- package/dist/commands/verify-hosted-reproducibility.d.ts +0 -90
- package/dist/commands/verify-hosted.d.ts +0 -92
- package/dist/commands/verify-mcp.d.ts +0 -8
- package/dist/commands/verify-mcp.js +0 -517
- package/dist/commands/verify-ops.d.ts +0 -158
- package/dist/commands/verify-ops.js +0 -1148
- package/dist/commands/verify-precompute.d.ts +0 -30
- package/dist/commands/verify-reproducibility.d.ts +0 -85
- package/dist/commands/verify-reproducibility.js +0 -494
- package/dist/commands/verify-reseal.d.ts +0 -9
- package/dist/commands/verify-reseal.js +0 -148
- package/dist/commands/verify-sandboxes.d.ts +0 -95
- package/dist/commands/verify-sandboxes.js +0 -352
- package/dist/commands/verify.d.ts +0 -28
- package/dist/commands/webhooks.d.ts +0 -30
- package/dist/index.d.ts +0 -22
- package/dist/schema.d.ts +0 -28
- package/dist/states.d.ts +0 -29
- package/dist/tools/detect.d.ts +0 -50
- package/dist/triage/prompts.d.ts +0 -24
- package/dist/triage/runner.d.ts +0 -34
- package/dist/triage/types.d.ts +0 -42
- package/dist/types/verify-history.d.ts +0 -38
- package/dist/types.d.ts +0 -1684
- package/dist/utils/action-output.d.ts +0 -24
- package/dist/utils/analysis-api.d.ts +0 -187
- package/dist/utils/auth.d.ts +0 -79
- package/dist/utils/config.d.ts +0 -24
- package/dist/utils/design-verifier-api.d.ts +0 -208
- package/dist/utils/design-verifier-history.d.ts +0 -46
- package/dist/utils/design-verifier-result.d.ts +0 -74
- package/dist/utils/detect.d.ts +0 -43
- package/dist/utils/git.d.ts +0 -135
- package/dist/utils/github-api.d.ts +0 -104
- package/dist/utils/haystack-api.d.ts +0 -37
- package/dist/utils/hooks.d.ts +0 -12
- package/dist/utils/pending-state.d.ts +0 -40
- package/dist/utils/pr-ref.d.ts +0 -27
- package/dist/utils/prompter.d.ts +0 -85
- package/dist/utils/secrets.d.ts +0 -47
- package/dist/utils/telemetry.d.ts +0 -19
- /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
|
-
|
|
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 (
|
|
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 (
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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,
|
|
306
|
+
export function defaultHostedIdempotencyKey(repository, baseCommit, headCommit, intent) {
|
|
369
307
|
const digest = createHash('sha256')
|
|
370
|
-
.update(JSON.stringify(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
443
|
-
const
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
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,
|
|
373
|
+
?? defaultHostedIdempotencyKey(repository.fullName, baseCommit, headCommit, intent));
|
|
459
374
|
const launchBody = {
|
|
460
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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.
|
|
464
|
+
const icon = state.status === 'complete'
|
|
554
465
|
? chalk.cyan('•')
|
|
555
|
-
: state.status
|
|
556
|
-
? chalk.
|
|
557
|
-
:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|