@haystackeditor/cli 0.15.31 → 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.
- package/README.md +111 -60
- 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 +608 -0
- package/dist/commands/case-batch.js +713 -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 +7 -2
- 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 +57 -10
- package/dist/commands/verify.js +51 -139
- package/dist/index.js +238 -350
- package/dist/lazy.js +8 -0
- package/dist/schema.js +1 -0
- package/dist/tools/detect.js +3 -24
- package/dist/triage/prompts.js +46 -45
- package/dist/triage/runner.js +119 -33
- package/dist/types.js +3 -3
- 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 +26 -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 +18 -14
- package/schemas/case-batch.v1.json +184 -0
- package/schemas/cloud-verifier.v1.json +55 -100
- 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
package/dist/commands/policy.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
12
12
|
import { join, dirname } from 'path';
|
|
13
|
-
import
|
|
13
|
+
import { confirm as confirmPrompt, input, select } from '@inquirer/prompts';
|
|
14
|
+
import { runPrompt } from '../utils/prompter.js';
|
|
14
15
|
const POLICY_PATH = '.haystack/review-policy.md';
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// Markdown Parsing
|
|
@@ -18,7 +19,7 @@ const POLICY_PATH = '.haystack/review-policy.md';
|
|
|
18
19
|
/**
|
|
19
20
|
* Parse review-policy.md content into ReviewPolicy objects and instructions
|
|
20
21
|
*/
|
|
21
|
-
function parsePolicyMarkdown(content) {
|
|
22
|
+
export function parsePolicyMarkdown(content) {
|
|
22
23
|
const policies = [];
|
|
23
24
|
const instructions = [];
|
|
24
25
|
// Split by H2 headings (## )
|
|
@@ -45,9 +46,11 @@ function parsePolicyMarkdown(content) {
|
|
|
45
46
|
let paths = [];
|
|
46
47
|
let severity = 'medium';
|
|
47
48
|
let reason = '';
|
|
48
|
-
// Parse
|
|
49
|
+
// Parse fields. serializePolicies (and therefore policy init/add/remove and
|
|
50
|
+
// setup) writes them as bullets, `- **Paths**: ...`; hand-written files may
|
|
51
|
+
// omit the bullet, so accept both.
|
|
49
52
|
for (const line of lines.slice(1)) {
|
|
50
|
-
const trimmed = line.trim();
|
|
53
|
+
const trimmed = line.trim().replace(/^[-*]\s+/, '');
|
|
51
54
|
// Match **Paths**: `pattern1`, `pattern2`
|
|
52
55
|
const pathsMatch = trimmed.match(/^\*\*Paths?\*\*:\s*(.+)$/i);
|
|
53
56
|
if (pathsMatch) {
|
|
@@ -84,7 +87,7 @@ function parsePolicyMarkdown(content) {
|
|
|
84
87
|
/**
|
|
85
88
|
* Serialize ReviewPolicy objects and instructions back to markdown
|
|
86
89
|
*/
|
|
87
|
-
function serializePolicies(policies, instructions = []) {
|
|
90
|
+
export function serializePolicies(policies, instructions = []) {
|
|
88
91
|
const lines = ['# Review Policies', ''];
|
|
89
92
|
if (instructions.length > 0) {
|
|
90
93
|
lines.push('## Instructions', '');
|
|
@@ -177,30 +180,24 @@ export async function addPolicy(nameArg) {
|
|
|
177
180
|
}
|
|
178
181
|
console.log(chalk.cyan('\n➕ Add Review Policy\n'));
|
|
179
182
|
// Prompt for policy details
|
|
180
|
-
const answers =
|
|
181
|
-
{
|
|
182
|
-
type: 'input',
|
|
183
|
-
name: 'name',
|
|
183
|
+
const answers = {
|
|
184
|
+
name: await runPrompt(input({
|
|
184
185
|
message: 'Policy name:',
|
|
185
186
|
default: nameArg,
|
|
186
|
-
validate: (
|
|
187
|
-
if (!
|
|
187
|
+
validate: (value) => {
|
|
188
|
+
if (!value.trim())
|
|
188
189
|
return 'Name is required';
|
|
189
|
-
if (policies.some(p => p.name.toLowerCase() ===
|
|
190
|
+
if (policies.some(p => p.name.toLowerCase() === value.toLowerCase())) {
|
|
190
191
|
return 'A policy with this name already exists';
|
|
191
192
|
}
|
|
192
193
|
return true;
|
|
193
194
|
},
|
|
194
|
-
},
|
|
195
|
-
{
|
|
196
|
-
type: 'input',
|
|
197
|
-
name: 'paths',
|
|
195
|
+
})),
|
|
196
|
+
paths: await runPrompt(input({
|
|
198
197
|
message: 'File patterns (comma-separated globs):',
|
|
199
|
-
validate: (
|
|
200
|
-
},
|
|
201
|
-
{
|
|
202
|
-
type: 'list',
|
|
203
|
-
name: 'severity',
|
|
198
|
+
validate: (value) => value.trim() ? true : 'At least one path pattern is required',
|
|
199
|
+
})),
|
|
200
|
+
severity: await runPrompt(select({
|
|
204
201
|
message: 'Severity:',
|
|
205
202
|
choices: [
|
|
206
203
|
{ name: chalk.red('critical') + ' - Always requires human review', value: 'critical' },
|
|
@@ -209,14 +206,12 @@ export async function addPolicy(nameArg) {
|
|
|
209
206
|
{ name: chalk.dim('low') + ' - Nice to review', value: 'low' },
|
|
210
207
|
],
|
|
211
208
|
default: 'medium',
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
type: 'input',
|
|
215
|
-
name: 'reason',
|
|
209
|
+
})),
|
|
210
|
+
reason: await runPrompt(input({
|
|
216
211
|
message: 'Reason for requiring review:',
|
|
217
|
-
validate: (
|
|
218
|
-
},
|
|
219
|
-
|
|
212
|
+
validate: (value) => value.trim() ? true : 'Reason is required',
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
220
215
|
// Parse paths from comma-separated input
|
|
221
216
|
const paths = answers.paths.split(',').map((p) => p.trim()).filter(Boolean);
|
|
222
217
|
const newPolicy = {
|
|
@@ -264,14 +259,10 @@ export async function removePolicy(name) {
|
|
|
264
259
|
// Confirm removal. Without a TTY the prompt would hang — the command names
|
|
265
260
|
// an exact policy, so treat the invocation itself as the confirmation.
|
|
266
261
|
if (process.stdin.isTTY) {
|
|
267
|
-
const
|
|
268
|
-
{
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
message: `Remove policy "${removed.name}"?`,
|
|
272
|
-
default: false,
|
|
273
|
-
},
|
|
274
|
-
]);
|
|
262
|
+
const confirm = await runPrompt(confirmPrompt({
|
|
263
|
+
message: `Remove policy "${removed.name}"?`,
|
|
264
|
+
default: false,
|
|
265
|
+
}));
|
|
275
266
|
if (!confirm) {
|
|
276
267
|
console.log(chalk.dim('\nCancelled.\n'));
|
|
277
268
|
return;
|
|
@@ -307,15 +298,11 @@ export async function addInstruction(text) {
|
|
|
307
298
|
'Pass the text directly: haystack policy add-instruction "Never flag weak test coverage"\n'));
|
|
308
299
|
process.exit(1);
|
|
309
300
|
}
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
validate: (input) => input.trim() ? true : 'Instruction text is required',
|
|
316
|
-
},
|
|
317
|
-
]);
|
|
318
|
-
instruction = answers.instruction.trim();
|
|
301
|
+
const answer = await runPrompt(input({
|
|
302
|
+
message: 'Review instruction:',
|
|
303
|
+
validate: (value) => value.trim() ? true : 'Instruction text is required',
|
|
304
|
+
}));
|
|
305
|
+
instruction = answer.trim();
|
|
319
306
|
}
|
|
320
307
|
instructions.push(instruction);
|
|
321
308
|
// Ensure directory exists
|
|
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { closeSync, constants, fchmodSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { validateCapturedSearch } from './combination-search-hook.js';
|
|
6
7
|
const requestFile = 'request.json';
|
|
7
8
|
const receiptFile = 'receipt.json';
|
|
8
9
|
const deliverySchema = 'precompute-delivery-v1';
|
|
@@ -126,7 +127,7 @@ function validateRequest(value) {
|
|
|
126
127
|
const request = value.request;
|
|
127
128
|
const component = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-';
|
|
128
129
|
if (!onlyKeys(request, ['owner', 'repository', 'baseCommit', 'workCommit', 'observedAt',
|
|
129
|
-
'cacheKey', 'patchGzBase64', 'patchSha256'])
|
|
130
|
+
'cacheKey', 'patchGzBase64', 'patchSha256', ...(request.combinationSearch === undefined ? [] : ['combinationSearch'])])
|
|
130
131
|
|| !characters(request.owner, component) || request.owner.length > 100
|
|
131
132
|
|| !characters(request.repository, component) || request.repository.length > 100
|
|
132
133
|
|| !characters(request.baseCommit, '0123456789abcdef', 40)
|
|
@@ -141,6 +142,10 @@ function validateRequest(value) {
|
|
|
141
142
|
|| createHash('sha256').update(patch).digest('hex') !== request.patchSha256) {
|
|
142
143
|
throw new Error('Captured patch identity does not match.');
|
|
143
144
|
}
|
|
145
|
+
if (request.combinationSearch !== undefined)
|
|
146
|
+
validateCapturedSearch(request.combinationSearch, {
|
|
147
|
+
owner: request.owner, repository: request.repository, baseCommit: request.baseCommit, workCommit: request.workCommit,
|
|
148
|
+
});
|
|
144
149
|
}
|
|
145
150
|
export class PrecomputeDeliveryAccessError extends Error {
|
|
146
151
|
delivery;
|
|
@@ -49,11 +49,11 @@ const UNSAFE_VALUE_PATTERNS = [
|
|
|
49
49
|
function sanitizedStringSchema(schema, label) {
|
|
50
50
|
return schema.superRefine((value, context) => {
|
|
51
51
|
if (value.includes('\0')) {
|
|
52
|
-
context.addIssue({ code:
|
|
52
|
+
context.addIssue({ code: 'custom', message: `${label} must not contain NUL` });
|
|
53
53
|
}
|
|
54
54
|
for (const [pattern, message] of UNSAFE_VALUE_PATTERNS) {
|
|
55
55
|
if (pattern.test(value)) {
|
|
56
|
-
context.addIssue({ code:
|
|
56
|
+
context.addIssue({ code: 'custom', message: `${label}: ${message}` });
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
});
|
|
@@ -84,13 +84,13 @@ const repositoryPathSchema = safeStringSchema('repository path').superRefine((va
|
|
|
84
84
|
normalize(value) !== value ||
|
|
85
85
|
value.split('/').some((component) => component === '..' || component === '')) {
|
|
86
86
|
context.addIssue({
|
|
87
|
-
code:
|
|
87
|
+
code: 'custom',
|
|
88
88
|
message: 'must be a normalized repository-relative POSIX path',
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
if (value.split('/').includes('.haystack')) {
|
|
92
92
|
context.addIssue({
|
|
93
|
-
code:
|
|
93
|
+
code: 'custom',
|
|
94
94
|
message: 'must not reference generated .haystack artifacts',
|
|
95
95
|
});
|
|
96
96
|
}
|
|
@@ -120,7 +120,7 @@ const evidenceSourceSchema = z
|
|
|
120
120
|
if (source.locatorKind === 'line-span') {
|
|
121
121
|
if (!source.lines || source.lines.end < source.lines.start) {
|
|
122
122
|
context.addIssue({
|
|
123
|
-
code:
|
|
123
|
+
code: 'custom',
|
|
124
124
|
path: ['lines'],
|
|
125
125
|
message: 'must contain positive start/end positions with end >= start for locatorKind line-span',
|
|
126
126
|
});
|
|
@@ -128,7 +128,7 @@ const evidenceSourceSchema = z
|
|
|
128
128
|
}
|
|
129
129
|
else if (source.lines !== undefined) {
|
|
130
130
|
context.addIssue({
|
|
131
|
-
code:
|
|
131
|
+
code: 'custom',
|
|
132
132
|
path: ['lines'],
|
|
133
133
|
message: 'is only valid for locatorKind line-span',
|
|
134
134
|
});
|
|
@@ -149,7 +149,7 @@ const evidenceRecordSchema = z
|
|
|
149
149
|
const budgetUsageSchema = z
|
|
150
150
|
.object({
|
|
151
151
|
repositoryInspectionCallsUsed: z.number().int().nonnegative(),
|
|
152
|
-
elapsedSeconds: z.number().
|
|
152
|
+
elapsedSeconds: z.number().nonnegative(),
|
|
153
153
|
})
|
|
154
154
|
.strict();
|
|
155
155
|
const provisionalUniverseScaffoldInputSchema = z
|
|
@@ -160,9 +160,7 @@ const provisionalUniverseScaffoldInputSchema = z
|
|
|
160
160
|
id: stableIdSchema,
|
|
161
161
|
requestedScope: stableIdSchema,
|
|
162
162
|
evaluationMode: z.literal('real-production', {
|
|
163
|
-
|
|
164
|
-
message: 'must be "real-production" because this scaffold records missing real-production authority',
|
|
165
|
-
}),
|
|
163
|
+
error: 'must be "real-production" because this scaffold records missing real-production authority',
|
|
166
164
|
}),
|
|
167
165
|
})
|
|
168
166
|
.strict(),
|
package/dist/commands/setup.js
CHANGED
|
@@ -15,7 +15,8 @@ import chalk from 'chalk';
|
|
|
15
15
|
import { execFileSync, execSync } from 'child_process';
|
|
16
16
|
import { basename } from 'path';
|
|
17
17
|
import { resolveAuthContext } from '../utils/auth.js';
|
|
18
|
-
import {
|
|
18
|
+
import { githubRestBaseFor } from '../utils/github-api.js';
|
|
19
|
+
import { describeHookOverwrites, hooksInstall } from './hooks.js';
|
|
19
20
|
import { installSessionHooks } from './install-session-hooks.js';
|
|
20
21
|
import { findGitRoot } from '../utils/hooks.js';
|
|
21
22
|
import { trackError } from '../utils/telemetry.js';
|
|
@@ -61,10 +62,9 @@ function buildAnswerMap(options) {
|
|
|
61
62
|
// Constants
|
|
62
63
|
// =============================================================================
|
|
63
64
|
const HAYSTACK_API = 'https://haystackeditor.com';
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
const GITHUB_API = 'https://api.github.com';
|
|
65
|
+
// The token verification call below uses githubRestBaseFor: direct to GitHub
|
|
66
|
+
// for an OAuth token (user-scoped, so OAuth App Policy does not apply), the
|
|
67
|
+
// Haystack proxy for an hsk_live token, which must never reach GitHub.
|
|
68
68
|
// Per-repo reads + writes go through the auth-worker proxy, which swaps in the
|
|
69
69
|
// GitHub App installation token server-side. That's what lets the config write
|
|
70
70
|
// and the bootstrap PR succeed on orgs that restrict the OAuth app — the same
|
|
@@ -1041,7 +1041,7 @@ async function runSetupFlow(options) {
|
|
|
1041
1041
|
}
|
|
1042
1042
|
const token = authContext.token;
|
|
1043
1043
|
// Verify token
|
|
1044
|
-
const userResponse = await fetch(`${
|
|
1044
|
+
const userResponse = await fetch(`${githubRestBaseFor(token)}/user`, {
|
|
1045
1045
|
headers: {
|
|
1046
1046
|
Authorization: `Bearer ${token}`,
|
|
1047
1047
|
'User-Agent': 'Haystack-CLI',
|
|
@@ -1073,7 +1073,7 @@ async function runSetupFlow(options) {
|
|
|
1073
1073
|
return;
|
|
1074
1074
|
}
|
|
1075
1075
|
// Step 7: Offer Haystack's local quality hooks when setup runs in a selected repo.
|
|
1076
|
-
await stepInstallHooks(selectedRepos);
|
|
1076
|
+
await stepInstallHooks(selectedRepos, options.json === true);
|
|
1077
1077
|
// Close the loop with ONE prominent CTA: the Haystack feed. The Configure
|
|
1078
1078
|
// Haystack PR (and every PR after it) is reviewed and merged from the feed,
|
|
1079
1079
|
// NOT the GitHub PR page, so the feed is the only link we surface. In JSON
|
|
@@ -1110,7 +1110,7 @@ async function runSetupFlow(options) {
|
|
|
1110
1110
|
// =============================================================================
|
|
1111
1111
|
// Step 7: Install Haystack git hooks
|
|
1112
1112
|
// =============================================================================
|
|
1113
|
-
async function stepInstallHooks(selectedRepos) {
|
|
1113
|
+
async function stepInstallHooks(selectedRepos, json) {
|
|
1114
1114
|
// Offer to install local git hooks if cwd is inside one of the selected repos
|
|
1115
1115
|
const gitRoot = findGitRoot();
|
|
1116
1116
|
if (gitRoot) {
|
|
@@ -1123,12 +1123,34 @@ async function stepInstallHooks(selectedRepos) {
|
|
|
1123
1123
|
try {
|
|
1124
1124
|
const installHooks = await ui.confirm({
|
|
1125
1125
|
id: 'install_hooks',
|
|
1126
|
-
message: `Install local hooks for ${repoName}? (
|
|
1126
|
+
message: `Install local hooks for ${repoName}? (git hooks in hooks/ via core.hooksPath; Claude Code session and verification-precompute hooks in your .claude/settings.local.json)`,
|
|
1127
1127
|
default: true,
|
|
1128
1128
|
});
|
|
1129
1129
|
if (installHooks) {
|
|
1130
1130
|
console.log('');
|
|
1131
|
-
|
|
1131
|
+
const overwrites = await describeHookOverwrites(gitRoot);
|
|
1132
|
+
let installGitHooks = true;
|
|
1133
|
+
if (overwrites.length > 0) {
|
|
1134
|
+
console.log(chalk.yellow(' Installing Haystack hooks would:'));
|
|
1135
|
+
for (const change of overwrites)
|
|
1136
|
+
console.log(chalk.yellow(` - ${change}`));
|
|
1137
|
+
if (json || !process.stdin.isTTY) {
|
|
1138
|
+
console.log(' Existing git hooks were left alone; to replace them, run `haystack hooks install --force` yourself.');
|
|
1139
|
+
installGitHooks = false;
|
|
1140
|
+
}
|
|
1141
|
+
else {
|
|
1142
|
+
installGitHooks = await ui.confirm({
|
|
1143
|
+
id: 'overwrite_hooks',
|
|
1144
|
+
message: 'Overwrite these existing hooks?',
|
|
1145
|
+
default: false,
|
|
1146
|
+
});
|
|
1147
|
+
if (!installGitHooks) {
|
|
1148
|
+
console.log(chalk.dim(' Existing git hooks were left alone. To replace them later: haystack hooks install --force'));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
if (installGitHooks)
|
|
1153
|
+
await hooksInstall({ force: overwrites.length > 0 });
|
|
1132
1154
|
await installSessionHooks({ cli: 'claude' });
|
|
1133
1155
|
}
|
|
1134
1156
|
}
|
package/dist/commands/submit.js
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import fs from 'fs';
|
|
14
|
-
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, } from '../utils/git.js';
|
|
14
|
+
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, HEADLESS_PUSH_ERROR, } from '../utils/git.js';
|
|
15
15
|
import { createPullRequest, findExistingPR, getRepositoryAccess, requestReviewers, } from '../utils/github-api.js';
|
|
16
16
|
import { runTriage } from '../triage/runner.js';
|
|
17
17
|
import { loadConfig } from '../utils/config.js';
|
|
18
18
|
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
19
19
|
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
20
20
|
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
|
-
import { listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
21
|
+
import { isHeadlessToken, listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
22
22
|
import { HaystackApiError } from '../utils/haystack-api.js';
|
|
23
23
|
import { withSchema } from '../schema.js';
|
|
24
24
|
// ============================================================================
|
|
@@ -316,6 +316,11 @@ export async function submitCommand(options) {
|
|
|
316
316
|
catch (err) {
|
|
317
317
|
fail(err.message);
|
|
318
318
|
}
|
|
319
|
+
// A headless (hsk_live_) token cannot authenticate a git push, and must not
|
|
320
|
+
// be sent to github.com as a password. Stop before triage and the push.
|
|
321
|
+
if (githubToken && isHeadlessToken(githubToken)) {
|
|
322
|
+
fail(HEADLESS_PUSH_ERROR);
|
|
323
|
+
}
|
|
319
324
|
// Check for uncommitted changes
|
|
320
325
|
if (hasUncommittedChanges()) {
|
|
321
326
|
console.log(chalk.yellow('⚠ You have uncommitted changes. They will not be included in the PR.'));
|
|
@@ -7,8 +7,20 @@ import chalk from 'chalk';
|
|
|
7
7
|
import fg from 'fast-glob';
|
|
8
8
|
// @babel/core intentionally has no bundled declarations. Keep this small
|
|
9
9
|
// build-transform boundary dynamic instead of leaking `any` into CLI contracts.
|
|
10
|
+
// It is loaded on first use, not at module top: loading it costs tens of
|
|
11
|
+
// milliseconds, and only the instrument transforms below need it.
|
|
10
12
|
const require = createRequire(import.meta.url);
|
|
11
|
-
|
|
13
|
+
let babelModule;
|
|
14
|
+
function loadBabel() {
|
|
15
|
+
babelModule ??= require('@babel/core');
|
|
16
|
+
return babelModule;
|
|
17
|
+
}
|
|
18
|
+
// @babel/core 8 is ESM-only. require() of ESM works from Node 22.12, but 22.12
|
|
19
|
+
// prints an ExperimentalWarning for it, so the command path loads Babel with
|
|
20
|
+
// import() first and loadBabel() then reuses that module.
|
|
21
|
+
async function preloadBabel() {
|
|
22
|
+
babelModule ??= await import('@babel/core');
|
|
23
|
+
}
|
|
12
24
|
const INSTRUMENTED_MARKER = '__haystack_telemetry_instrumented_v1__';
|
|
13
25
|
const INSTRUMENTED_MARKER_COMMENT = `/* ${INSTRUMENTED_MARKER} */`;
|
|
14
26
|
const BOOTSTRAP_MARKER = '__haystack_telemetry_bootstrap_v1__';
|
|
@@ -486,6 +498,7 @@ function instrumentSource(code, sourcePath, runtimeSpecifier, moduleType, contro
|
|
|
486
498
|
},
|
|
487
499
|
};
|
|
488
500
|
};
|
|
501
|
+
const babel = loadBabel();
|
|
489
502
|
const transformed = babel.transformSync(code, {
|
|
490
503
|
parserOpts: {
|
|
491
504
|
sourceType: 'unambiguous',
|
|
@@ -541,7 +554,7 @@ function findNearestPackageType(start, stop) {
|
|
|
541
554
|
current = dirname(current);
|
|
542
555
|
}
|
|
543
556
|
}
|
|
544
|
-
function moduleTypeForOutput(output,
|
|
557
|
+
function moduleTypeForOutput(output, _dist) {
|
|
545
558
|
const extension = extname(output);
|
|
546
559
|
if (extension === '.mjs')
|
|
547
560
|
return 'module';
|
|
@@ -570,6 +583,7 @@ function bootstrapFor(entry, dist, buildId, runtimeHash, runtimeGuardChannel, ru
|
|
|
570
583
|
return `/* ${BOOTSTRAP_MARKER} */ (() => { const processHost = require('node:process'); const define = (name, descriptor) => ({}).constructor.defineProperty(processHost, name, descriptor); const block = () => { try { if (!(${JSON.stringify(runtimeBlockedChannel)} in processHost)) define(${JSON.stringify(runtimeBlockedChannel)}, { configurable: false, enumerable: false, writable: false, value: true }); } catch (_ignored) {} }; try { if (${JSON.stringify(runtimeGuardChannel)} in processHost || ${JSON.stringify(runtimeBlockedChannel)} in processHost) { block(); return; } const runtimePath = require.resolve(${JSON.stringify(specifier)}); const source = require('node:fs').readFileSync(runtimePath, 'utf8'); if (require('node:crypto').createHash('sha256').update(source).digest('hex') !== ${JSON.stringify(runtimeHash)}) { block(); return; } const Module = require('node:module'); const loaded = new Module(runtimePath); loaded.filename = runtimePath; loaded.paths = Module._nodeModulePaths(require('node:path').dirname(runtimePath)); loaded._compile(source, runtimePath); loaded.loaded = true; const runtime = loaded.exports; if (!runtime || typeof runtime.install !== 'function' || typeof runtime.safeSelectProbe !== 'function') { block(); return; } define(${JSON.stringify(runtimeGuardChannel)}, { configurable: false, enumerable: false, writable: false, value: runtime }); require.cache[runtimePath] = loaded; runtime.install({ buildId: ${JSON.stringify(buildId)} }); } catch (_error) { block(); } })();\n`;
|
|
571
584
|
}
|
|
572
585
|
function insertEntryBootstrap(code, bootstrap) {
|
|
586
|
+
const babel = loadBabel();
|
|
573
587
|
const bootstrapAst = babel.parseSync(bootstrap, {
|
|
574
588
|
sourceType: 'unambiguous',
|
|
575
589
|
});
|
|
@@ -814,6 +828,7 @@ export async function telemetryInstrumentCommand(distDirectory, options) {
|
|
|
814
828
|
}
|
|
815
829
|
includeSymbols = rows;
|
|
816
830
|
}
|
|
831
|
+
await preloadBabel();
|
|
817
832
|
const result = instrumentNodeTelemetryBuild(distDirectory, { ...options, includeSymbols });
|
|
818
833
|
if (options.json) {
|
|
819
834
|
console.log(JSON.stringify(result, null, 2));
|
package/dist/commands/triage.js
CHANGED
|
@@ -28,6 +28,7 @@ import { parsePrRef } from '../utils/pr-ref.js';
|
|
|
28
28
|
import { toPublicVerdict } from '../states.js';
|
|
29
29
|
import { withSchema } from '../schema.js';
|
|
30
30
|
import { fetchGitHubJson } from '../utils/haystack-api.js';
|
|
31
|
+
import { githubRestBaseFor } from '../utils/github-api.js';
|
|
31
32
|
// ============================================================================
|
|
32
33
|
// Output formatting
|
|
33
34
|
// ============================================================================
|
|
@@ -562,7 +563,7 @@ function updatePendingSubmitIfExists(pr, status) {
|
|
|
562
563
|
*/
|
|
563
564
|
async function recordFindingsSeen(pr, token, findingKeys) {
|
|
564
565
|
// Get PR head SHA
|
|
565
|
-
const ghResp = await fetch(
|
|
566
|
+
const ghResp = await fetch(`${githubRestBaseFor(token)}/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, {
|
|
566
567
|
headers: {
|
|
567
568
|
Authorization: `Bearer ${token}`,
|
|
568
569
|
Accept: 'application/vnd.github.v3+json',
|
|
@@ -12,6 +12,7 @@ function timestamp(value) {
|
|
|
12
12
|
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
13
13
|
}
|
|
14
14
|
function terminalSafe(value) {
|
|
15
|
+
// eslint-disable-next-line no-control-regex -- rejecting/stripping control characters is the point
|
|
15
16
|
return value.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g, '');
|
|
16
17
|
}
|
|
17
18
|
async function context(runId, repository) {
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
// Production Cloud Verifier MCP server: `haystack verify hosted mcp`.
|
|
2
2
|
//
|
|
3
|
-
// This intentionally exposes only the durable hosted run lifecycle. The local
|
|
4
|
-
// specimen MCP server has artifact and retained-sandbox tools whose data model
|
|
5
|
-
// does not exist in the source-free production result contract.
|
|
6
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
7
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
8
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
@@ -58,13 +55,9 @@ export const HOSTED_VERIFY_TOOLS = [
|
|
|
58
55
|
pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
|
|
59
56
|
description: 'Optional stable retry key. Omit to derive it from the exact repository and commits.',
|
|
60
57
|
},
|
|
61
|
-
replay_plan_from_run_id: {
|
|
62
|
-
...runIdInput,
|
|
63
|
-
description: 'Optional completed owned run whose exact validated risk plan must be replayed.',
|
|
64
|
-
},
|
|
65
58
|
account: accountInput,
|
|
66
59
|
},
|
|
67
|
-
required: ['repository', 'base_commit', 'head_commit'
|
|
60
|
+
required: ['repository', 'base_commit', 'head_commit'],
|
|
68
61
|
},
|
|
69
62
|
},
|
|
70
63
|
{
|
|
@@ -130,7 +123,6 @@ function assertToolInputFields(name, input) {
|
|
|
130
123
|
'head_commit',
|
|
131
124
|
'idempotency_key',
|
|
132
125
|
'intent_file',
|
|
133
|
-
'replay_plan_from_run_id',
|
|
134
126
|
'repository',
|
|
135
127
|
]);
|
|
136
128
|
return;
|
|
@@ -157,9 +149,8 @@ export async function executeHostedVerifyTool(name, input, context) {
|
|
|
157
149
|
const started = await startHostedVerification(repository, {
|
|
158
150
|
base: requireInputString(input, 'base_commit'),
|
|
159
151
|
head: requireInputString(input, 'head_commit'),
|
|
160
|
-
intentFile:
|
|
152
|
+
intentFile: optionalInputString(input, 'intent_file'),
|
|
161
153
|
idempotencyKey: optionalInputString(input, 'idempotency_key'),
|
|
162
|
-
replayPlanFrom: optionalInputString(input, 'replay_plan_from_run_id'),
|
|
163
154
|
wait: false,
|
|
164
155
|
}, context.token);
|
|
165
156
|
return buildHostedJson('start', started.state, {
|
|
@@ -224,7 +215,7 @@ export async function runHostedVerifyMcpServer(options) {
|
|
|
224
215
|
// Reject undeclared fields before account resolution so callers cannot
|
|
225
216
|
// use an invalid request to trigger credential or repository lookup.
|
|
226
217
|
assertToolInputFields(name, input);
|
|
227
|
-
if (name === 'verify_start') {
|
|
218
|
+
if (name === 'verify_start' && input.intent_file !== undefined) {
|
|
228
219
|
readHostedIntentFile(requireInputString(input, 'intent_file'));
|
|
229
220
|
}
|
|
230
221
|
const context = await resolveToolContext(name, input, options);
|