@haystackeditor/cli 0.15.27 → 0.15.28

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 (52) hide show
  1. package/README.md +13 -20
  2. package/dist/assets/hooks/agent-context/detect.ts +7 -7
  3. package/dist/assets/hooks/agent-context/parsers/claude.ts +1 -1
  4. package/dist/assets/hooks/scripts/commit-msg.sh +1 -3
  5. package/dist/assets/hooks/scripts/post-commit.sh +1 -3
  6. package/dist/assets/hooks/scripts/pre-push.sh +1 -4
  7. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +1 -2
  8. package/dist/assets/telemetry/browser-runtime.js +651 -43
  9. package/dist/assets/telemetry/runtime.cjs +62 -68
  10. package/dist/commands/design-verify.d.ts +11 -3
  11. package/dist/commands/design-verify.js +147 -4
  12. package/dist/commands/hooks.d.ts +1 -5
  13. package/dist/commands/hooks.js +10 -112
  14. package/dist/commands/install-session-hooks.d.ts +2 -2
  15. package/dist/commands/install-session-hooks.js +71 -21
  16. package/dist/commands/mcp.js +1 -34
  17. package/dist/commands/pr.js +2 -7
  18. package/dist/commands/setup.d.ts +1 -2
  19. package/dist/commands/setup.js +18 -272
  20. package/dist/commands/submit.js +1 -2
  21. package/dist/commands/verify-hosted.d.ts +13 -0
  22. package/dist/commands/verify-hosted.js +49 -1
  23. package/dist/commands/verify-precompute.d.ts +19 -0
  24. package/dist/commands/verify-precompute.js +328 -0
  25. package/dist/index.js +37 -38
  26. package/dist/schema.d.ts +1 -2
  27. package/dist/schema.js +1 -2
  28. package/dist/triage/prompts.d.ts +0 -7
  29. package/dist/triage/prompts.js +0 -145
  30. package/dist/triage/runner.js +3 -22
  31. package/dist/triage/types.d.ts +1 -8
  32. package/dist/types.d.ts +8 -8
  33. package/dist/types.js +1 -1
  34. package/dist/utils/design-verifier-api.d.ts +71 -0
  35. package/dist/utils/design-verifier-api.js +96 -0
  36. package/dist/utils/design-verifier-result.d.ts +7 -2
  37. package/dist/utils/design-verifier-result.js +67 -21
  38. package/dist/utils/git.d.ts +5 -1
  39. package/dist/utils/git.js +19 -9
  40. package/dist/utils/github-api.js +11 -14
  41. package/dist/utils/hooks.d.ts +1 -15
  42. package/dist/utils/hooks.js +1 -127
  43. package/dist/utils/telemetry.d.ts +2 -0
  44. package/dist/utils/telemetry.js +81 -8
  45. package/package.json +2 -1
  46. package/schemas/cloud-verifier.v1.json +69 -3
  47. package/schemas/{pr.v2.json → pr.v3.json} +3 -12
  48. package/dist/commands/traces.d.ts +0 -48
  49. package/dist/commands/traces.js +0 -92
  50. package/dist/triage/traces.d.ts +0 -20
  51. package/dist/triage/traces.js +0 -311
  52. package/schemas/traces.v1.json +0 -53
@@ -1,12 +1,15 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
2
  function findingExplanation(kind) {
3
3
  if (kind === 'declared-change-not-observed') {
4
- return 'The intended behavior change was not observed on the proposed revision.';
4
+ return 'The registered intended behavior change was not observed; investigate the captured outputs before concluding why.';
5
5
  }
6
6
  if (kind === 'unexpected-change') {
7
- return 'The proposed revision changed behavior that was registered to remain stable.';
7
+ return 'The proposed revision produced a difference outside this case’s registered intent; this is an anomaly candidate for investigation.';
8
8
  }
9
- return 'Executed behavior contradicted the registered expectation.';
9
+ if (kind === 'intended-change-control-confounded') {
10
+ return 'The intended difference was observed, but control-before also varied on an intersecting output path; inspect it before claiming causality.';
11
+ }
12
+ return 'Executed behavior produced a suspicious difference against the registered expectation.';
10
13
  }
11
14
  function sameRecordedValue(left, right) {
12
15
  return isDeepStrictEqual(left, right);
@@ -19,6 +22,14 @@ function executionSummaries(value) {
19
22
  ['completed', 'failed', 'missing'].includes(execution.status) &&
20
23
  Array.isArray(execution.diagnostics) && Array.isArray(execution.outputExcerpt));
21
24
  }
25
+ function universeSummaries(value, caseId) {
26
+ if (!Array.isArray(value))
27
+ return [];
28
+ return value.filter((universe) => universe?.version === 'dv-case-universe-summary-v1' && universe.caseId === caseId &&
29
+ /^universe-[0-9a-f]{24}$/.test(universe.universeId || '') &&
30
+ ['control-before', 'before', 'after'].includes(universe.role) &&
31
+ universe.lease && typeof universe.lease.available === 'boolean');
32
+ }
22
33
  export function buildDesignVerifierAgentReport(run) {
23
34
  const repository = `${run.owner}/${run.repo}`;
24
35
  if (run.status !== 'succeeded' || !run.result) {
@@ -46,13 +57,19 @@ export function buildDesignVerifierAgentReport(run) {
46
57
  const summary = verdict.summary || {};
47
58
  const findings = Array.isArray(verdict.findings) ? verdict.findings : [];
48
59
  const confirmations = Array.isArray(verdict.confirmations) ? verdict.confirmations : [];
49
- const findingByCase = new Map(findings.map((finding) => [finding.caseId, finding]));
60
+ const findingsByCase = new Map();
61
+ for (const finding of findings) {
62
+ const caseFindings = findingsByCase.get(String(finding?.caseId || '')) || [];
63
+ caseFindings.push(finding);
64
+ findingsByCase.set(String(finding?.caseId || ''), caseFindings);
65
+ }
50
66
  const confirmedIds = new Set(confirmations.map((confirmation) => confirmation.caseId));
51
67
  const scenarioByCase = new Map(run.result.scenarios.map((scenario) => [scenario.caseId, scenario]));
52
68
  const probeByCase = new Map(run.result.probes.map((probe) => [probe.caseId, probe]));
53
69
  const cases = run.result.cases.map((testCase) => {
54
70
  const id = String(testCase.id || '');
55
- const finding = findingByCase.get(id);
71
+ const caseFindings = findingsByCase.get(id) || [];
72
+ const finding = caseFindings[0];
56
73
  const scenario = scenarioByCase.get(id);
57
74
  const probe = probeByCase.get(id);
58
75
  const observations = (Array.isArray(scenario?.divergences) ? scenario.divergences : []).map((item) => ({
@@ -60,13 +77,18 @@ export function buildDesignVerifierAgentReport(run) {
60
77
  base: item.base,
61
78
  baseRepeat: item.base2,
62
79
  head: item.head,
80
+ controlObservedAtSamePath: item.controlObservedAtSamePath === true,
81
+ controlObservedAtAncestorPath: item.controlObservedAtAncestorPath === true,
82
+ controlObservedAtDescendantPath: item.controlObservedAtDescendantPath === true,
63
83
  }));
84
+ const hasControlEvidence = typeof scenario?.noisePathCount === 'number' ||
85
+ scenario?.controlScenarioErrorVariation != null;
64
86
  return {
65
87
  id,
66
88
  title: String(testCase.title || scenario?.scenario || id),
67
89
  story: String(testCase.story || ''),
68
90
  expectedHead: String(testCase.expected_head_observation || ''),
69
- verdict: finding
91
+ verdict: caseFindings.length
70
92
  ? 'finding'
71
93
  : confirmedIds.has(id)
72
94
  ? 'confirmed'
@@ -74,13 +96,19 @@ export function buildDesignVerifierAgentReport(run) {
74
96
  ? 'clean'
75
97
  : 'inconclusive',
76
98
  findingKind: finding?.kind || null,
77
- baseStable: observations.length
78
- ? observations.every((item) => sameRecordedValue(item.base, item.baseRepeat))
79
- : scenario?.status === 'clean' ? true : null,
99
+ findingKinds: caseFindings
100
+ .map((item) => String(item?.kind || ''))
101
+ .filter(Boolean),
102
+ baseStable: hasControlEvidence
103
+ ? scenario.noisePathCount === 0 && scenario.controlScenarioErrorVariation == null
104
+ : observations.length
105
+ ? observations.every((item) => sameRecordedValue(item.base, item.baseRepeat))
106
+ : scenario?.status === 'clean' ? true : null,
80
107
  observations,
81
108
  generatedFiles: probe?.files || [],
82
109
  executedCommand: probe?.run || null,
83
110
  executions: executionSummaries(scenario?.executions),
111
+ universes: universeSummaries(scenario?.universes, id),
84
112
  infrastructure: scenario?.infrastructureReceipt &&
85
113
  typeof scenario.infrastructureReceipt === 'object'
86
114
  ? {
@@ -106,8 +134,8 @@ export function buildDesignVerifierAgentReport(run) {
106
134
  let explanation;
107
135
  let exitCode;
108
136
  if (findings.length > 0) {
109
- agentVerdict = 'fix_needed';
110
- headline = 'FIX NEEDED';
137
+ agentVerdict = 'anomaly_candidate';
138
+ headline = 'ANOMALY CANDIDATE';
111
139
  explanation = findingExplanation(String(findings[0]?.kind || ''));
112
140
  exitCode = 1;
113
141
  }
@@ -120,13 +148,13 @@ export function buildDesignVerifierAgentReport(run) {
120
148
  else if (confirmations.length > 0) {
121
149
  agentVerdict = 'fix_confirmed';
122
150
  headline = 'FIX CONFIRMED';
123
- explanation = 'The intended behavior change was observed on head and reproduced across two stable base runs.';
151
+ explanation = 'The intended behavior change was observed on output paths that did not vary between control-before and before.';
124
152
  exitCode = 0;
125
153
  }
126
154
  else {
127
155
  agentVerdict = 'no_unexpected_change';
128
156
  headline = 'NO UNEXPECTED CHANGE';
129
- explanation = 'Executed cases found no behavior change outside the registered expectations.';
157
+ explanation = 'The executed cases found no observed difference outside their registered expectations.';
130
158
  exitCode = 0;
131
159
  }
132
160
  return {
@@ -184,10 +212,10 @@ function commandLabel(command) {
184
212
  }
185
213
  function executionSideLabel(side) {
186
214
  if (side === 'base1')
187
- return 'base';
215
+ return 'before';
188
216
  if (side === 'base2')
189
- return 'base repeat';
190
- return 'head';
217
+ return 'control-before';
218
+ return 'after';
191
219
  }
192
220
  function executionNeedsAttention(execution) {
193
221
  return execution.status !== 'completed' || execution.diagnostics.length > 0;
@@ -200,7 +228,7 @@ export function formatDesignVerifierAgentReport(report) {
200
228
  report.explanation,
201
229
  ];
202
230
  if (report.revisions.base || report.revisions.head) {
203
- lines.push('', `Revisions: base ${report.revisions.base || 'unknown'} (executed twice)`, ` head ${report.revisions.head || 'unknown'} (executed once)`);
231
+ lines.push('', `Revisions: before ${report.revisions.base || 'unknown'} (control + comparison)`, ` after ${report.revisions.head || 'unknown'} (comparison)`);
204
232
  }
205
233
  if (report.timings) {
206
234
  lines.push('', 'Timing breakdown:');
@@ -221,16 +249,30 @@ export function formatDesignVerifierAgentReport(report) {
221
249
  lines.push(` Behavior: ${testCase.story}`);
222
250
  if (testCase.expectedHead)
223
251
  lines.push(` Expected on head: ${testCase.expectedHead}`);
252
+ if (testCase.findingKinds.length > 0) {
253
+ lines.push(` Candidate reasons: ${testCase.findingKinds.join(', ')}`);
254
+ }
224
255
  if (testCase.observations.length) {
225
256
  lines.push(' Observed:');
226
257
  for (const observation of testCase.observations) {
227
258
  lines.push(` ${observation.path}`);
228
- lines.push(` base: ${displayValue(observation.base)} (repeat: ${displayValue(observation.baseRepeat)})`);
229
- lines.push(` head: ${displayValue(observation.head)}`);
259
+ lines.push(` control-before: ${displayValue(observation.baseRepeat)}`);
260
+ lines.push(` before: ${displayValue(observation.base)}`);
261
+ lines.push(` after: ${displayValue(observation.head)}`);
262
+ if (observation.controlObservedAtSamePath || observation.controlObservedAtAncestorPath ||
263
+ observation.controlObservedAtDescendantPath) {
264
+ lines.push(` control evidence: ${observation.controlObservedAtSamePath
265
+ ? 'variation observed at this same path'
266
+ : observation.controlObservedAtAncestorPath
267
+ ? 'variation observed at an ancestor path'
268
+ : 'variation observed at a descendant path'}`);
269
+ }
230
270
  }
231
271
  }
232
272
  if (testCase.baseStable !== null) {
233
- lines.push(` Base stability: ${testCase.baseStable ? 'passed; both runs agreed' : 'failed; base runs disagreed'}`);
273
+ lines.push(` Control evidence: ${testCase.baseStable
274
+ ? 'control-before and before agreed'
275
+ : 'control-before and before varied; inspect alongside the difference'}`);
234
276
  }
235
277
  if (testCase.infrastructure) {
236
278
  lines.push(testCase.infrastructure.verified
@@ -271,6 +313,10 @@ export function formatDesignVerifierAgentReport(report) {
271
313
  const command = commandLabel(testCase.executedCommand);
272
314
  if (command)
273
315
  lines.push(` Executed in the sandbox: ${command}`);
316
+ for (const universe of testCase.universes.filter(item => item.lease.available)) {
317
+ lines.push(` Replay ${universe.role}: haystack design-verify ${report.repository}#${report.pullRequest}` +
318
+ ` --run ${report.runId} --case ${testCase.id} --universe ${universe.role} --replay`);
319
+ }
274
320
  }
275
321
  if (report.changedFiles.length) {
276
322
  lines.push('', 'Changed files to inspect:', ...report.changedFiles.map((file) => ` ${file}`));
@@ -292,7 +338,7 @@ export function designVerifierStageLabel(stage) {
292
338
  if (stage.startsWith('build:'))
293
339
  return 'Building an executable probe';
294
340
  if (stage.startsWith('execute:'))
295
- return 'Running base/base/head comparisons';
341
+ return 'Running control-before/before/after comparisons';
296
342
  if (stage === 'verdict')
297
343
  return 'Comparing observations';
298
344
  return stage;
@@ -93,6 +93,10 @@ export declare function getLastCommitSha(): string;
93
93
  * Returns the combined log, or empty string if no commits or on error.
94
94
  */
95
95
  export declare function getCommitMessagesSinceBase(baseBranch: string): string;
96
+ interface ResolveDiffBaseRefOptions {
97
+ cwd?: string;
98
+ timeoutMs?: number;
99
+ }
96
100
  /**
97
101
  * Resolve the git ref to diff a PR branch against for review.
98
102
  *
@@ -124,7 +128,7 @@ export declare function getCommitMessagesSinceBase(baseBranch: string): string;
124
128
  * 4. Fall back to whatever single qualified ref resolves, then the bare
125
129
  * <base> name (preserves prior behavior rather than breaking triage).
126
130
  */
127
- export declare function resolveDiffBaseRef(baseBranch: string): string;
131
+ export declare function resolveDiffBaseRef(baseBranch: string, options?: ResolveDiffBaseRefOptions): string;
128
132
  /**
129
133
  * Get the number of commits ahead of base branch
130
134
  */
package/dist/utils/git.js CHANGED
@@ -376,11 +376,18 @@ export function getCommitMessagesSinceBase(baseBranch) {
376
376
  }
377
377
  }
378
378
  }
379
+ function gitCommandOptions(options) {
380
+ return {
381
+ ...(options.cwd ? { cwd: options.cwd } : {}),
382
+ ...(options.timeoutMs ? { timeout: options.timeoutMs } : {}),
383
+ stdio: ['pipe', 'pipe', 'pipe'],
384
+ };
385
+ }
379
386
  /** True if `ref` resolves to a commit in the current repo. */
380
- function gitRefResolves(ref) {
387
+ function gitRefResolves(ref, options = {}) {
381
388
  try {
382
389
  execFileSync('git', ['rev-parse', '--verify', '--quiet', '--', `${ref}^{commit}`], {
383
- stdio: ['pipe', 'pipe', 'pipe'],
390
+ ...gitCommandOptions(options),
384
391
  });
385
392
  return true;
386
393
  }
@@ -389,11 +396,11 @@ function gitRefResolves(ref) {
389
396
  }
390
397
  }
391
398
  /** True if `ancestor` is an ancestor of (or equal to) `descendant`. */
392
- function gitIsAncestor(ancestor, descendant) {
399
+ function gitIsAncestor(ancestor, descendant, options = {}) {
393
400
  try {
394
401
  // exit 0 => ancestor; exit 1 => not; both non-throwing for us via stdio pipe
395
402
  execFileSync('git', ['merge-base', '--is-ancestor', '--', ancestor, descendant], {
396
- stdio: ['pipe', 'pipe', 'pipe'],
403
+ ...gitCommandOptions(options),
397
404
  });
398
405
  return true;
399
406
  }
@@ -432,7 +439,7 @@ function gitIsAncestor(ancestor, descendant) {
432
439
  * 4. Fall back to whatever single qualified ref resolves, then the bare
433
440
  * <base> name (preserves prior behavior rather than breaking triage).
434
441
  */
435
- export function resolveDiffBaseRef(baseBranch) {
442
+ export function resolveDiffBaseRef(baseBranch, options = {}) {
436
443
  const remoteRef = `refs/remotes/origin/${baseBranch}`;
437
444
  const localRef = `refs/heads/${baseBranch}`;
438
445
  // Freshen the remote base so the merge-base reflects the true fork point.
@@ -451,7 +458,10 @@ export function resolveDiffBaseRef(baseBranch) {
451
458
  let fetched = false;
452
459
  try {
453
460
  execFileSync('git', ['fetch', 'origin', `+refs/heads/${baseBranch}:${remoteRef}`], {
454
- stdio: ['pipe', 'pipe', 'pipe'],
461
+ ...gitCommandOptions(options),
462
+ env: options.timeoutMs
463
+ ? { ...process.env, GIT_TERMINAL_PROMPT: '0' }
464
+ : process.env,
455
465
  });
456
466
  fetched = true;
457
467
  }
@@ -459,17 +469,17 @@ export function resolveDiffBaseRef(baseBranch) {
459
469
  // Offline / no creds / base not on remote — fall through to degraded
460
470
  // selection below using whatever refs already exist locally.
461
471
  }
462
- const remoteResolves = gitRefResolves(remoteRef);
472
+ const remoteResolves = gitRefResolves(remoteRef, options);
463
473
  // Happy path: we just refreshed the remote base, so it's authoritative.
464
474
  if (fetched && remoteResolves)
465
475
  return remoteRef;
466
476
  // Degraded path: fetch failed (or didn't produce the ref). Prefer the more
467
477
  // up-to-date of the cached remote ref and the local branch; only a base that
468
478
  // lags HEAD's fork point balloons the diff, so pick the one that's ahead.
469
- const localResolves = gitRefResolves(localRef);
479
+ const localResolves = gitRefResolves(localRef, options);
470
480
  if (remoteResolves && localResolves) {
471
481
  // If the remote ref is an ancestor of the local ref, local is ahead → use it.
472
- return gitIsAncestor(remoteRef, localRef) ? localRef : remoteRef;
482
+ return gitIsAncestor(remoteRef, localRef, options) ? localRef : remoteRef;
473
483
  }
474
484
  if (remoteResolves)
475
485
  return remoteRef;
@@ -86,10 +86,7 @@ async function githubApiRequest(method, path, token, body) {
86
86
  const proxied = await fetch(`${GITHUB_PROXY_BASE}${path}`, init);
87
87
  return proxied.status === 403 ? direct : proxied;
88
88
  }
89
- /**
90
- * Handle API errors consistently
91
- */
92
- async function handleApiError(response, context) {
89
+ async function handleApiError(response, context, operation) {
93
90
  if (response.status === 401) {
94
91
  throw new Error('GitHub token expired or invalid. Run `haystack login` again.');
95
92
  }
@@ -100,7 +97,7 @@ async function handleApiError(response, context) {
100
97
  // whether the CLI needs its own token bucket. Routes to PostHog (and on to
101
98
  // Slack via the ops-alert pipeline). Fire-and-forget; never blocks.
102
99
  trackError('cli_github_rate_limited', {
103
- context,
100
+ operation,
104
101
  status: 403,
105
102
  rate_limit_limit: response.headers.get('x-ratelimit-limit'),
106
103
  rate_limit_reset: response.headers.get('x-ratelimit-reset'),
@@ -149,7 +146,7 @@ export async function createPullRequest(options) {
149
146
  `Push to a new branch name and try again.`);
150
147
  }
151
148
  }
152
- await handleApiError(response, `creating PR in ${options.owner}/${options.repo}`);
149
+ await handleApiError(response, `creating PR in ${options.owner}/${options.repo}`, 'create_pull_request');
153
150
  }
154
151
  return response.json();
155
152
  }
@@ -190,7 +187,7 @@ export async function findExistingPR(owner, repo, head, token) {
190
187
  if (!response.ok) {
191
188
  if (response.status === 404)
192
189
  return null;
193
- await handleApiError(response, `checking for existing PR`);
190
+ await handleApiError(response, 'checking for existing PR', 'find_existing_pull_request');
194
191
  }
195
192
  const prs = await response.json();
196
193
  return prs.length > 0 ? prs[0] : null;
@@ -205,7 +202,7 @@ export async function addLabelsToIssue(owner, repo, issueNumber, labels, token)
205
202
  const authToken = await resolveToken(token, { owner, repo });
206
203
  const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken, { labels });
207
204
  if (!response.ok) {
208
- await handleApiError(response, `adding labels to PR #${issueNumber}`);
205
+ await handleApiError(response, `adding labels to PR #${issueNumber}`, 'add_labels');
209
206
  }
210
207
  }
211
208
  /**
@@ -215,7 +212,7 @@ export async function getLabelsFromIssue(owner, repo, issueNumber, token) {
215
212
  const authToken = await resolveToken(token, { owner, repo });
216
213
  const response = await githubApiRequest('GET', `/repos/${owner}/${repo}/issues/${issueNumber}/labels`, authToken);
217
214
  if (!response.ok) {
218
- await handleApiError(response, `getting labels from PR #${issueNumber}`);
215
+ await handleApiError(response, `getting labels from PR #${issueNumber}`, 'get_labels');
219
216
  }
220
217
  const labels = await response.json();
221
218
  return labels.map((l) => l.name);
@@ -228,7 +225,7 @@ export async function removeLabelFromIssue(owner, repo, issueNumber, labelName,
228
225
  const response = await githubApiRequest('DELETE', `/repos/${owner}/${repo}/issues/${issueNumber}/labels/${encodeURIComponent(labelName)}`, authToken);
229
226
  // 404 is fine - label wasn't on the issue
230
227
  if (!response.ok && response.status !== 404) {
231
- await handleApiError(response, `removing label '${labelName}' from PR #${issueNumber}`);
228
+ await handleApiError(response, `removing label '${labelName}' from PR #${issueNumber}`, 'remove_label');
232
229
  }
233
230
  }
234
231
  /**
@@ -251,7 +248,7 @@ export async function createLabel(owner, repo, label, token) {
251
248
  });
252
249
  if (!response.ok && response.status !== 422) {
253
250
  // 422 means label already exists, which is fine
254
- await handleApiError(response, `creating label '${label.name}'`);
251
+ await handleApiError(response, `creating label '${label.name}'`, 'create_label');
255
252
  }
256
253
  }
257
254
  /**
@@ -278,7 +275,7 @@ export async function requestReviewers(owner, repo, prNumber, reviewers, token)
278
275
  const authToken = await resolveToken(token, { owner, repo });
279
276
  const response = await githubApiRequest('POST', `/repos/${owner}/${repo}/pulls/${prNumber}/requested_reviewers`, authToken, { reviewers });
280
277
  if (!response.ok) {
281
- await handleApiError(response, `requesting reviewers on PR #${prNumber}`);
278
+ await handleApiError(response, `requesting reviewers on PR #${prNumber}`, 'request_reviewers');
282
279
  }
283
280
  }
284
281
  // ============================================================================
@@ -291,7 +288,7 @@ export async function getCurrentUser(token) {
291
288
  const authToken = await resolveToken(token);
292
289
  const response = await githubApiRequest('GET', '/user', authToken);
293
290
  if (!response.ok) {
294
- await handleApiError(response, 'getting current user');
291
+ await handleApiError(response, 'getting current user', 'get_current_user');
295
292
  }
296
293
  return response.json();
297
294
  }
@@ -299,7 +296,7 @@ export async function getRepositoryAccess(owner, repo, token) {
299
296
  const authToken = await resolveToken(token, { owner, repo });
300
297
  const response = await githubApiRequest('GET', `/repos/${owner}/${repo}`, authToken);
301
298
  if (!response.ok) {
302
- await handleApiError(response, `getting repository access for ${owner}/${repo}`);
299
+ await handleApiError(response, `getting repository access for ${owner}/${repo}`, 'get_repository_access');
303
300
  }
304
301
  return response.json();
305
302
  }
@@ -1,17 +1,7 @@
1
1
  /**
2
- * Utilities for haystack hooks install/status/update commands.
3
- *
4
- * Handles Entire CLI binary download, hook file copying, and git configuration.
2
+ * Utilities for haystack hooks install/status commands.
5
3
  */
6
- export declare const ENTIRE_DEFAULT_VERSION = "0.4.8";
7
- export declare const ENTIRE_GITHUB_REPO = "entireio/cli";
8
- export declare const HAYSTACK_BIN_DIR: string;
9
- export declare const ENTIRE_BIN_PATH: string;
10
- export declare function getPlatformAssetName(): string;
11
4
  export declare function findGitRoot(): string | null;
12
- export declare function getInstalledEntireVersion(): Promise<string | null>;
13
- export declare function getLatestEntireVersion(): Promise<string>;
14
- export declare function downloadEntireBinary(version: string): Promise<void>;
15
5
  /** Resolves the path to dist/assets/hooks/ (or src/assets/hooks/ in dev) */
16
6
  export declare function getAssetsDir(): string;
17
7
  /** Copy hook scripts and TypeScript modules into the target hooks directory */
@@ -20,7 +10,3 @@ export declare function copyHookFiles(hooksDir: string): Promise<void>;
20
10
  export declare function installHookDeps(hooksDir: string): Promise<void>;
21
11
  /** Copy LLM rules template if none exists */
22
12
  export declare function copyLlmRulesTemplate(repoRoot: string): Promise<boolean>;
23
- /** Create .entire/settings.json and .entire/.gitignore */
24
- export declare function createEntireConfig(repoRoot: string): Promise<void>;
25
- /** Add .entire/metadata/ to repo .gitignore if not already present */
26
- export declare function updateGitignore(repoRoot: string): Promise<boolean>;
@@ -1,21 +1,14 @@
1
1
  /**
2
- * Utilities for haystack hooks install/status/update commands.
3
- *
4
- * Handles Entire CLI binary download, hook file copying, and git configuration.
2
+ * Utilities for haystack hooks install/status commands.
5
3
  */
6
4
  import * as fs from 'fs/promises';
7
5
  import { existsSync } from 'fs';
8
6
  import * as path from 'path';
9
- import * as os from 'os';
10
7
  import { execSync } from 'child_process';
11
8
  import { fileURLToPath } from 'url';
12
9
  // ============================================================================
13
10
  // Constants
14
11
  // ============================================================================
15
- export const ENTIRE_DEFAULT_VERSION = '0.4.8';
16
- export const ENTIRE_GITHUB_REPO = 'entireio/cli';
17
- export const HAYSTACK_BIN_DIR = path.join(os.homedir(), '.haystack', 'bin');
18
- export const ENTIRE_BIN_PATH = path.join(HAYSTACK_BIN_DIR, 'entire');
19
12
  const HOOK_SCRIPTS = [
20
13
  'pre-commit',
21
14
  'commit-msg',
@@ -24,25 +17,6 @@ const HOOK_SCRIPTS = [
24
17
  'prepare-commit-msg',
25
18
  ];
26
19
  // ============================================================================
27
- // Platform detection
28
- // ============================================================================
29
- const OS_MAP = {
30
- darwin: 'darwin',
31
- linux: 'linux',
32
- };
33
- const ARCH_MAP = {
34
- arm64: 'arm64',
35
- x64: 'amd64',
36
- };
37
- export function getPlatformAssetName() {
38
- const osName = OS_MAP[process.platform];
39
- const archName = ARCH_MAP[process.arch];
40
- if (!osName || !archName) {
41
- throw new Error(`Unsupported platform: ${process.platform}-${process.arch}. Entire CLI supports macOS and Linux (amd64/arm64).`);
42
- }
43
- return `entire_${osName}_${archName}.tar.gz`;
44
- }
45
- // ============================================================================
46
20
  // Git repo detection
47
21
  // ============================================================================
48
22
  export function findGitRoot() {
@@ -57,86 +31,6 @@ export function findGitRoot() {
57
31
  }
58
32
  }
59
33
  // ============================================================================
60
- // Entire binary management
61
- // ============================================================================
62
- export async function getInstalledEntireVersion() {
63
- if (!existsSync(ENTIRE_BIN_PATH))
64
- return null;
65
- try {
66
- const output = execSync(`"${ENTIRE_BIN_PATH}" --version`, {
67
- encoding: 'utf-8',
68
- stdio: ['pipe', 'pipe', 'pipe'],
69
- }).trim();
70
- // Output is like "entire version 0.4.8" or just "0.4.8"
71
- const match = output.match(/(\d+\.\d+\.\d+)/);
72
- return match ? match[1] : output;
73
- }
74
- catch {
75
- return null;
76
- }
77
- }
78
- export async function getLatestEntireVersion() {
79
- const response = await fetch(`https://api.github.com/repos/${ENTIRE_GITHUB_REPO}/releases/latest`, {
80
- headers: {
81
- 'User-Agent': 'Haystack-CLI',
82
- Accept: 'application/vnd.github.v3+json',
83
- },
84
- });
85
- if (!response.ok) {
86
- throw new Error(`Failed to fetch latest Entire release: ${response.status} ${response.statusText}`);
87
- }
88
- const data = (await response.json());
89
- // tag_name is like "v0.4.8"
90
- return data.tag_name.replace(/^v/, '');
91
- }
92
- export async function downloadEntireBinary(version) {
93
- const assetName = getPlatformAssetName();
94
- const tag = version.startsWith('v') ? version : `v${version}`;
95
- // Fetch release to get asset download URL
96
- const releaseResponse = await fetch(`https://api.github.com/repos/${ENTIRE_GITHUB_REPO}/releases/tags/${tag}`, {
97
- headers: {
98
- 'User-Agent': 'Haystack-CLI',
99
- Accept: 'application/vnd.github.v3+json',
100
- },
101
- });
102
- if (!releaseResponse.ok) {
103
- throw new Error(`Failed to fetch Entire release ${tag}: ${releaseResponse.status}`);
104
- }
105
- const release = (await releaseResponse.json());
106
- const asset = release.assets.find((a) => a.name === assetName);
107
- if (!asset) {
108
- throw new Error(`No binary found for ${assetName} in Entire release ${tag}. Available: ${release.assets.map((a) => a.name).join(', ')}`);
109
- }
110
- // Download the tar.gz
111
- const downloadResponse = await fetch(asset.browser_download_url, {
112
- headers: { 'User-Agent': 'Haystack-CLI' },
113
- });
114
- if (!downloadResponse.ok) {
115
- throw new Error(`Failed to download ${assetName}: ${downloadResponse.status}`);
116
- }
117
- const buffer = Buffer.from(await downloadResponse.arrayBuffer());
118
- // Create temp dir, extract, and move binary
119
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'haystack-entire-'));
120
- const tarPath = path.join(tmpDir, assetName);
121
- try {
122
- await fs.writeFile(tarPath, buffer);
123
- execSync(`tar xzf "${tarPath}" -C "${tmpDir}"`, { stdio: 'pipe' });
124
- // Ensure bin dir exists
125
- await fs.mkdir(HAYSTACK_BIN_DIR, { recursive: true });
126
- // Move the binary
127
- const extractedBinary = path.join(tmpDir, 'entire');
128
- if (!existsSync(extractedBinary)) {
129
- throw new Error('Entire binary not found in extracted archive');
130
- }
131
- await fs.copyFile(extractedBinary, ENTIRE_BIN_PATH);
132
- await fs.chmod(ENTIRE_BIN_PATH, 0o755);
133
- }
134
- finally {
135
- // Clean up temp dir
136
- await fs.rm(tmpDir, { recursive: true, force: true });
137
- }
138
- }
139
- // ============================================================================
140
34
  // Asset directory resolution
141
35
  // ============================================================================
142
36
  const __filename = fileURLToPath(import.meta.url);
@@ -188,26 +82,6 @@ export async function copyLlmRulesTemplate(repoRoot) {
188
82
  await fs.copyFile(src, dest);
189
83
  return true;
190
84
  }
191
- /** Create .entire/settings.json and .entire/.gitignore */
192
- export async function createEntireConfig(repoRoot) {
193
- const entireDir = path.join(repoRoot, '.entire');
194
- await fs.mkdir(entireDir, { recursive: true });
195
- await fs.writeFile(path.join(entireDir, 'settings.json'), JSON.stringify({ enabled: true, telemetry: false }, null, 2) + '\n', 'utf-8');
196
- await fs.writeFile(path.join(entireDir, '.gitignore'), 'tmp/\nsettings.local.json\nmetadata/\nlogs/\n', 'utf-8');
197
- }
198
- /** Add .entire/metadata/ to repo .gitignore if not already present */
199
- export async function updateGitignore(repoRoot) {
200
- const gitignorePath = path.join(repoRoot, '.gitignore');
201
- let content = '';
202
- if (existsSync(gitignorePath)) {
203
- content = await fs.readFile(gitignorePath, 'utf-8');
204
- }
205
- if (content.includes('.entire/metadata'))
206
- return false;
207
- const addition = '\n# Entire CLI local session data\n.entire/metadata/\n';
208
- await fs.writeFile(gitignorePath, content + addition, 'utf-8');
209
- return true;
210
- }
211
85
  // ============================================================================
212
86
  // Helpers
213
87
  // ============================================================================
@@ -15,3 +15,5 @@ export declare function trackError(errorType: string, metadata?: Record<string,
15
15
  * Track a setup event (non-error). Fire-and-forget.
16
16
  */
17
17
  export declare function trackSetupEvent(step: string, metadata?: Record<string, unknown>): void;
18
+ /** Track one bounded front-half precompute result per CLI invocation. */
19
+ export declare function trackPrecomputeEvent(outcome: string, wallMs: number, timeoutMs: number): void;