@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
@@ -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 inquirer from 'inquirer';
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 bullet points
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 = await inquirer.prompt([
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: (input) => {
187
- if (!input.trim())
187
+ validate: (value) => {
188
+ if (!value.trim())
188
189
  return 'Name is required';
189
- if (policies.some(p => p.name.toLowerCase() === input.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: (input) => input.trim() ? true : 'At least one path pattern is required',
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: (input) => input.trim() ? true : 'Reason is required',
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 { confirm } = await inquirer.prompt([
268
- {
269
- type: 'confirm',
270
- name: 'confirm',
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 answers = await inquirer.prompt([
311
- {
312
- type: 'input',
313
- name: 'instruction',
314
- message: 'Review instruction:',
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: z.ZodIssueCode.custom, message: `${label} must not contain NUL` });
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: z.ZodIssueCode.custom, message: `${label}: ${message}` });
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: z.ZodIssueCode.custom,
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: z.ZodIssueCode.custom,
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: z.ZodIssueCode.custom,
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: z.ZodIssueCode.custom,
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().finite().nonnegative(),
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
- errorMap: () => ({
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(),
@@ -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 { hooksInstall } from './hooks.js';
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
- // User-level GitHub calls (repo listing, App-installation check, token
65
- // verification) go direct: they're scoped to the authenticated user, so OAuth
66
- // App Policy org restrictions don't apply to them.
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(`${GITHUB_API}/user`, {
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}? (agent checks, session status, verification precompute)`,
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
- await hooksInstall({ force: true });
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
  }
@@ -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
- const babel = require('@babel/core');
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, dist) {
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));
@@ -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(`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, {
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) {
@@ -0,0 +1,152 @@
1
+ import { resolveAuthContext } from '../utils/auth.js';
2
+ import { haystackJson, HaystackApiError } from '../utils/haystack-api.js';
3
+ const HEALTH = ['healthy', 'degraded', 'infrastructure-error', 'not-executed', 'unknown'];
4
+ const LIFECYCLES = ['complete', 'failed', 'cancelled', 'budget-stopped', 'account-action'];
5
+ const RUN_ID = /^cv_[0-9a-f]{48}$/;
6
+ const COMMIT = /^[0-9a-f]{40}$/;
7
+ const REPO_COMPONENT = /^(?!\.{1,2}$)[A-Za-z0-9_.-]{1,100}$/;
8
+ function record(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+ function count(value) {
12
+ return Number.isSafeInteger(value) && value >= 0;
13
+ }
14
+ function timestamp(value) {
15
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
16
+ }
17
+ /** Reject contradictory or mismatched data before printing health or findings. */
18
+ export function parseVerifyHistoryReport(value, expected) {
19
+ const invalid = () => {
20
+ throw new Error('The hosted verifier returned invalid or mismatched history. No reliability summary was accepted.');
21
+ };
22
+ if (!record(value) || value.version !== 'dv-verify-history-v1'
23
+ || value.provider !== 'haystack' || value.scope !== 'user'
24
+ || !record(value.repository)
25
+ || typeof value.repository.owner !== 'string' || typeof value.repository.name !== 'string'
26
+ || value.repository.owner !== expected.owner
27
+ || value.repository.name !== expected.name
28
+ || typeof value.repository.repositoryId !== 'string'
29
+ || !/^github\.com:[1-9]\d*$/.test(value.repository.repositoryId)
30
+ || value.limit !== expected.limit || typeof value.hasMore !== 'boolean'
31
+ || !Array.isArray(value.runs) || value.runs.length > expected.limit
32
+ || (value.hasMore && value.runs.length !== expected.limit)
33
+ || !record(value.summary) || !record(value.summary.counts))
34
+ return invalid();
35
+ const counts = {
36
+ healthy: 0, degraded: 0, 'infrastructure-error': 0, 'not-executed': 0, unknown: 0,
37
+ };
38
+ const seen = new Set();
39
+ let usable = 0;
40
+ let previousCompletedAt = Infinity;
41
+ for (const run of value.runs) {
42
+ if (!record(run) || typeof run.runId !== 'string' || !RUN_ID.test(run.runId)
43
+ || seen.has(run.runId)
44
+ || typeof run.baseCommit !== 'string' || !COMMIT.test(run.baseCommit)
45
+ || typeof run.headCommit !== 'string' || !COMMIT.test(run.headCommit)
46
+ || !timestamp(run.createdAt) || !timestamp(run.completedAt)
47
+ || Date.parse(run.completedAt) < Date.parse(run.createdAt)
48
+ || Date.parse(run.completedAt) > previousCompletedAt
49
+ || (run.status !== 'complete' && run.status !== 'errored')
50
+ || (run.lifecycle !== null && !LIFECYCLES.includes(run.lifecycle))
51
+ || !HEALTH.includes(run.health)
52
+ || (run.regressionCount !== null && !count(run.regressionCount))
53
+ || (run.executedCaseCount !== null && !count(run.executedCaseCount))
54
+ || !Array.isArray(run.degradationCodes)
55
+ || run.degradationCodes.some(code => typeof code !== 'string' || code.length === 0)
56
+ || new Set(run.degradationCodes).size !== run.degradationCodes.length
57
+ || (run.supersededBy !== null && (typeof run.supersededBy !== 'string'
58
+ || !RUN_ID.test(run.supersededBy) || run.supersededBy === run.runId)))
59
+ return invalid();
60
+ if (run.health === 'unknown') {
61
+ if (run.status !== 'errored' || run.lifecycle !== null || run.regressionCount !== null
62
+ || run.executedCaseCount !== null || run.degradationCodes.length !== 0)
63
+ return invalid();
64
+ }
65
+ else if (run.lifecycle === null || run.regressionCount === null || run.executedCaseCount === null) {
66
+ return invalid();
67
+ }
68
+ else if ((run.lifecycle === 'complete') !== (run.status === 'complete')) {
69
+ return invalid();
70
+ }
71
+ if (run.health === 'healthy' && (run.lifecycle !== 'complete'
72
+ || run.executedCaseCount === 0 || run.degradationCodes.length !== 0))
73
+ return invalid();
74
+ if (run.health === 'not-executed' && (run.executedCaseCount !== 0 || run.regressionCount !== 0))
75
+ return invalid();
76
+ if (run.executedCaseCount === 0 && run.regressionCount !== null && run.regressionCount !== 0)
77
+ return invalid();
78
+ seen.add(run.runId);
79
+ previousCompletedAt = Date.parse(run.completedAt);
80
+ counts[run.health]++;
81
+ if ((run.health === 'healthy' || run.health === 'degraded')
82
+ && typeof run.executedCaseCount === 'number' && run.executedCaseCount > 0)
83
+ usable++;
84
+ }
85
+ const usableRate = value.runs.length === 0 ? null : usable / value.runs.length;
86
+ const summaryCounts = value.summary.counts;
87
+ if (value.summary.total !== value.runs.length || value.summary.usableRate !== usableRate
88
+ || HEALTH.some(health => summaryCounts[health] !== counts[health]))
89
+ return invalid();
90
+ return value;
91
+ }
92
+ function terminalSafe(value) {
93
+ return Array.from(value).filter(character => {
94
+ const point = character.codePointAt(0);
95
+ return point >= 32 && !(point >= 127 && point <= 159);
96
+ }).join('');
97
+ }
98
+ export function formatVerifyHistory(report) {
99
+ const { summary } = report;
100
+ const lines = [
101
+ `Your verification history for ${report.repository.owner}/${report.repository.name}`,
102
+ `Latest ${summary.total} finished runs for the selected account (limit ${report.limit}).`,
103
+ ];
104
+ if (summary.total === 0) {
105
+ lines.push('No finished hosted fleet runs found for this account and repository.');
106
+ return lines.join('\n');
107
+ }
108
+ const usable = report.runs.filter(run => (run.health === 'healthy' || run.health === 'degraded')
109
+ && run.executedCaseCount !== null && run.executedCaseCount > 0).length;
110
+ lines.push(`Usable evidence: ${Math.round(summary.usableRate * 100)}% of these runs (${usable}/${summary.total}).`, `Healthy: ${summary.counts.healthy}; partially degraded: ${summary.counts.degraded}; infrastructure error: ${summary.counts['infrastructure-error']}; not executed: ${summary.counts['not-executed']}; unknown: ${summary.counts.unknown}.`, 'Usable means healthy or partially degraded with at least one executed check.', 'This measures execution reliability. Bugs found are listed separately; they do not lower this rate.', '');
111
+ for (const run of report.runs) {
112
+ const health = run.health === 'degraded' ? 'partially degraded' : run.health;
113
+ const checks = run.executedCaseCount === null ? 'unknown checks executed' : `${run.executedCaseCount} checks executed`;
114
+ const regressions = run.regressionCount === null ? 'bugs found unknown'
115
+ : `${run.regressionCount} ${run.regressionCount === 1 ? 'bug' : 'bugs'} found`;
116
+ lines.push(`${terminalSafe(run.completedAt)} ${health} ${checks} ${regressions}`, ` ${run.runId} ${run.baseCommit.slice(0, 12)} -> ${run.headCommit.slice(0, 12)}`);
117
+ if (run.lifecycle && run.lifecycle !== 'complete')
118
+ lines.push(` Run outcome: ${run.lifecycle}`);
119
+ if (run.degradationCodes.length)
120
+ lines.push(` Limitations: ${run.degradationCodes.map(terminalSafe).join(', ')}`);
121
+ if (run.supersededBy)
122
+ lines.push(` Replaced by: ${run.supersededBy}`);
123
+ }
124
+ if (report.hasMore)
125
+ lines.push('', 'Older finished runs exist. Increase --limit to show up to 100.');
126
+ return lines.join('\n');
127
+ }
128
+ export async function verifyHistoryCommand(repository, options) {
129
+ const parts = repository.split('/');
130
+ if (parts.length !== 2 || !parts.every(part => REPO_COMPONENT.test(part))) {
131
+ throw new Error('Use an exact GitHub owner/repository name for verification history.');
132
+ }
133
+ const limitText = options.limit ?? '20';
134
+ if (!/^[1-9]\d*$/.test(limitText) || Number(limitText) > 100) {
135
+ throw new Error('--limit must be a whole number from 1 to 100.');
136
+ }
137
+ const [owner, name] = parts;
138
+ const limit = Number(limitText);
139
+ const auth = await resolveAuthContext({ preferredLogin: options.account, owner, repo: name });
140
+ let response;
141
+ try {
142
+ response = await haystackJson(`/api/agent/cloud-verifier/history/${encodeURIComponent(owner)}/${encodeURIComponent(name)}?limit=${limit}`, auth.token, { method: 'GET' });
143
+ }
144
+ catch (error) {
145
+ if (error instanceof HaystackApiError && error.status === 404) {
146
+ throw new Error('Verification history was not found for this repository and account. Check repository access and use --account <login> if needed.');
147
+ }
148
+ throw error;
149
+ }
150
+ const report = parseVerifyHistoryReport(response, { owner, name, limit });
151
+ process.stdout.write((options.json ? JSON.stringify(report, null, 2) : formatVerifyHistory(report)) + '\n');
152
+ }
@@ -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', 'intent_file'],
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: requireInputString(input, 'intent_file'),
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);