@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
@@ -11,23 +11,64 @@ import { readFileSync } from 'node:fs';
11
11
  import { dirname, resolve } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import * as os from 'os';
14
+ import { createHash } from 'node:crypto';
14
15
  const POSTHOG_HOST = 'https://us.i.posthog.com';
15
16
  const POSTHOG_API_KEY = process.env.HAYSTACK_POSTHOG_KEY || 'phc_7wHrCoMlcwuftLe6N38hPb9wKHb4OK0wNw9ayeRySad';
16
17
  const CURRENT_FILE_DIR = dirname(fileURLToPath(import.meta.url));
17
18
  const PACKAGE_JSON_PATH = resolve(CURRENT_FILE_DIR, '../../package.json');
19
+ const SAFE_ENUM = /^[A-Za-z0-9 _.:/-]{1,100}$/;
20
+ const SAFE_STRING_KEYS = new Set([
21
+ 'checker',
22
+ 'cli',
23
+ 'during',
24
+ 'kind',
25
+ 'operation',
26
+ 'outcome',
27
+ 'rate_limit_limit',
28
+ 'rate_limit_reset',
29
+ 'rate_limit_resource',
30
+ ]);
31
+ const SAFE_NUMBER_KEYS = new Set([
32
+ 'configured',
33
+ 'exit_code',
34
+ 'max_turns',
35
+ 'status',
36
+ 'timeout_ms',
37
+ 'total',
38
+ 'wall_ms',
39
+ ]);
40
+ function telemetryDisabled() {
41
+ return process.env.DO_NOT_TRACK === '1' || process.env.HAYSTACK_TELEMETRY_DISABLED === '1';
42
+ }
43
+ function minimizeMetadata(metadata) {
44
+ const minimized = {};
45
+ for (const [key, value] of Object.entries(metadata ?? {})) {
46
+ if (typeof value === 'string' && SAFE_STRING_KEYS.has(key) && SAFE_ENUM.test(value)) {
47
+ minimized[key] = value;
48
+ }
49
+ else if (typeof value === 'number' && Number.isFinite(value) && SAFE_NUMBER_KEYS.has(key)) {
50
+ minimized[key] = value;
51
+ }
52
+ else if (typeof value === 'boolean') {
53
+ minimized[key] = value;
54
+ }
55
+ }
56
+ return minimized;
57
+ }
18
58
  /**
19
- * Stable machine identifier for PostHog distinct_id.
20
- * Uses hostname + username so events are attributable per-machine.
59
+ * Pseudonymous stable machine identifier for PostHog distinct_id.
60
+ * The local username and hostname are never transmitted.
21
61
  * Never throws — falls back to hostname-only or 'anonymous'.
22
62
  */
23
63
  function getDistinctId() {
24
64
  try {
25
65
  const hostname = os.hostname();
26
66
  try {
27
- return `cli:${os.userInfo().username}@${hostname}`;
67
+ const localMachine = `${os.userInfo().username}\0${hostname}`;
68
+ return `cli:${createHash('sha256').update(localMachine).digest('hex').slice(0, 24)}`;
28
69
  }
29
70
  catch {
30
- return `cli:${hostname}`;
71
+ return `cli:${createHash('sha256').update(hostname).digest('hex').slice(0, 24)}`;
31
72
  }
32
73
  }
33
74
  catch {
@@ -39,7 +80,7 @@ function getDistinctId() {
39
80
  */
40
81
  export function trackError(errorType, metadata) {
41
82
  try {
42
- if (typeof globalThis.fetch !== 'function')
83
+ if (telemetryDisabled() || typeof globalThis.fetch !== 'function')
43
84
  return;
44
85
  const event = {
45
86
  api_key: POSTHOG_API_KEY,
@@ -52,7 +93,8 @@ export function trackError(errorType, metadata) {
52
93
  arch: process.arch,
53
94
  node_version: process.version,
54
95
  timestamp: new Date().toISOString(),
55
- ...metadata,
96
+ ...minimizeMetadata(metadata),
97
+ $ip: null,
56
98
  },
57
99
  timestamp: new Date().toISOString(),
58
100
  };
@@ -72,7 +114,7 @@ export function trackError(errorType, metadata) {
72
114
  */
73
115
  export function trackSetupEvent(step, metadata) {
74
116
  try {
75
- if (typeof globalThis.fetch !== 'function')
117
+ if (telemetryDisabled() || typeof globalThis.fetch !== 'function')
76
118
  return;
77
119
  const event = {
78
120
  api_key: POSTHOG_API_KEY,
@@ -84,7 +126,8 @@ export function trackSetupEvent(step, metadata) {
84
126
  os: process.platform,
85
127
  arch: process.arch,
86
128
  timestamp: new Date().toISOString(),
87
- ...metadata,
129
+ ...minimizeMetadata(metadata),
130
+ $ip: null,
88
131
  },
89
132
  timestamp: new Date().toISOString(),
90
133
  };
@@ -98,6 +141,36 @@ export function trackSetupEvent(step, metadata) {
98
141
  // Telemetry must never crash the CLI
99
142
  }
100
143
  }
144
+ /** Track one bounded front-half precompute result per CLI invocation. */
145
+ export function trackPrecomputeEvent(outcome, wallMs, timeoutMs) {
146
+ try {
147
+ if (telemetryDisabled() || typeof globalThis.fetch !== 'function')
148
+ return;
149
+ const event = {
150
+ api_key: POSTHOG_API_KEY,
151
+ distinct_id: getDistinctId(),
152
+ event: 'cli_verify_precompute',
153
+ properties: {
154
+ cli_version: getCLIVersion(),
155
+ os: process.platform,
156
+ arch: process.arch,
157
+ timestamp: new Date().toISOString(),
158
+ ...minimizeMetadata({ outcome, wall_ms: wallMs }),
159
+ $ip: null,
160
+ },
161
+ timestamp: new Date().toISOString(),
162
+ };
163
+ globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
164
+ method: 'POST',
165
+ headers: { 'Content-Type': 'application/json' },
166
+ body: JSON.stringify(event),
167
+ signal: AbortSignal.timeout(Math.max(1, timeoutMs)),
168
+ }).catch(() => { });
169
+ }
170
+ catch {
171
+ // Telemetry must never crash or delay the CLI.
172
+ }
173
+ }
101
174
  function getCLIVersion() {
102
175
  try {
103
176
  const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.27",
3
+ "version": "0.15.28",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "start": "node dist/index.js",
13
13
  "benchmark:telemetry": "pnpm build && node scripts/benchmark-telemetry-overhead.mjs",
14
14
  "benchmark:telemetry:corpus": "pnpm build && node scripts/benchmark-open-source-telemetry.mjs",
15
+ "benchmark:telemetry:corpus:guard": "pnpm build && node scripts/benchmark-open-source-telemetry.mjs --quick --require-budget",
15
16
  "check:schemas": "node scripts/check-schemas.mjs",
16
17
  "release": "node scripts/release.mjs",
17
18
  "prepublishOnly": "node scripts/no-direct-publish.mjs",
@@ -67,6 +67,61 @@
67
67
  },
68
68
  "error": {
69
69
  "type": "string"
70
+ },
71
+ "result": {
72
+ "type": "object",
73
+ "additionalProperties": false,
74
+ "required": [
75
+ "verdict",
76
+ "summary",
77
+ "casesTotal",
78
+ "casesFailed",
79
+ "regressions",
80
+ "outputLocation",
81
+ "wallSeconds"
82
+ ],
83
+ "properties": {
84
+ "verdict": {
85
+ "type": "string"
86
+ },
87
+ "summary": {
88
+ "type": "string"
89
+ },
90
+ "casesTotal": {
91
+ "type": "integer",
92
+ "minimum": 0
93
+ },
94
+ "casesFailed": {
95
+ "type": "integer",
96
+ "minimum": 0
97
+ },
98
+ "regressions": {
99
+ "type": "array",
100
+ "items": {
101
+ "type": "object",
102
+ "additionalProperties": false,
103
+ "required": [
104
+ "caseId",
105
+ "title"
106
+ ],
107
+ "properties": {
108
+ "caseId": {
109
+ "type": "string"
110
+ },
111
+ "title": {
112
+ "type": "string"
113
+ }
114
+ }
115
+ }
116
+ },
117
+ "outputLocation": {
118
+ "type": "string"
119
+ },
120
+ "wallSeconds": {
121
+ "type": "number",
122
+ "minimum": 0
123
+ }
124
+ }
70
125
  }
71
126
  },
72
127
  "allOf": [
@@ -86,9 +141,20 @@
86
141
  "repository",
87
142
  "base_commit",
88
143
  "head_commit",
89
- "created_at",
90
- "completed_at",
91
- "output"
144
+ "created_at"
145
+ ],
146
+ "anyOf": [
147
+ {
148
+ "required": [
149
+ "completed_at",
150
+ "output"
151
+ ]
152
+ },
153
+ {
154
+ "required": [
155
+ "result"
156
+ ]
157
+ }
92
158
  ]
93
159
  }
94
160
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://docs.haystackeditor.com/cli/schemas/pr.v2.json",
3
+ "$id": "https://docs.haystackeditor.com/cli/schemas/pr.v3.json",
4
4
  "title": "haystack pr get <ref> --json",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["schema_version", "ref", "title", "head_sha", "triage", "merge", "traces"],
7
+ "required": ["schema_version", "ref", "title", "head_sha", "triage", "merge"],
8
8
  "properties": {
9
- "schema_version": { "type": "string", "const": "2.0.0" },
9
+ "schema_version": { "type": "string", "const": "3.0.0" },
10
10
  "ref": { "type": "string" },
11
11
  "title": { "type": "string" },
12
12
  "head_sha": { "type": "string" },
@@ -31,15 +31,6 @@
31
31
  "action": { "type": ["string", "null"] }
32
32
  }
33
33
  },
34
- "traces": {
35
- "type": "object",
36
- "additionalProperties": false,
37
- "required": ["available", "count"],
38
- "properties": {
39
- "available": { "type": "boolean" },
40
- "count": { "type": "integer", "minimum": 0 }
41
- }
42
- },
43
34
  "fixer": {
44
35
  "type": "object",
45
36
  "additionalProperties": false,
@@ -1,48 +0,0 @@
1
- export interface TraceGetOptions {
2
- json?: boolean;
3
- cursor?: string;
4
- limit?: string;
5
- }
6
- export declare function fetchTracesListPayload(ref: string): Promise<{
7
- operation: string;
8
- ref: string;
9
- head_sha: string;
10
- checkpoints: {
11
- checkpoint_id: string;
12
- commit_sha: string;
13
- commit_message: string;
14
- timestamp: string | null;
15
- summary: string;
16
- user_prompt: string | null;
17
- files_touched: string[];
18
- branch: string | null;
19
- }[];
20
- } & {
21
- schema_version: string;
22
- }>;
23
- export declare function tracesListCommand(ref: string, options: {
24
- json?: boolean;
25
- }): Promise<void>;
26
- export declare function fetchTracesGetPayload(ref: string, checkpointId: string, options: Pick<TraceGetOptions, 'cursor' | 'limit'>): Promise<{
27
- operation: string;
28
- ref: string;
29
- head_sha: string;
30
- checkpoint_id: string;
31
- checkpoint: {
32
- checkpoint_id: string;
33
- commit_sha: string;
34
- commit_message: string;
35
- timestamp: string | null;
36
- summary: string;
37
- user_prompt: string | null;
38
- files_touched: string[];
39
- branch: string | null;
40
- } | null;
41
- chunks: string[];
42
- total_chunks: number;
43
- total_chars: number;
44
- next_cursor: string | null;
45
- } & {
46
- schema_version: string;
47
- }>;
48
- export declare function tracesGetCommand(ref: string, checkpointId: string, options: TraceGetOptions): Promise<void>;
@@ -1,92 +0,0 @@
1
- import chalk from 'chalk';
2
- import { loadToken } from '../utils/auth.js';
3
- import { fetchAnalysisArtifact, fetchGitHubJson } from '../utils/haystack-api.js';
4
- import { withSchema } from '../schema.js';
5
- import { parsePrRef } from '../utils/pr-ref.js';
6
- function publicCheckpoint(trace) {
7
- return {
8
- checkpoint_id: trace.checkpointId,
9
- commit_sha: trace.commitSha,
10
- commit_message: trace.commitMessage,
11
- timestamp: trace.timestamp ?? null,
12
- summary: trace.overviewSummary,
13
- user_prompt: trace.userPrompt ?? null,
14
- files_touched: trace.filesTouched ?? [],
15
- branch: trace.branch ?? null,
16
- };
17
- }
18
- async function tokenAndHead(ref) {
19
- const pr = parsePrRef(ref, 'traces list');
20
- const token = await loadToken({ owner: pr.owner, repo: pr.repo });
21
- if (!token)
22
- throw new Error('Not authenticated. Run `haystack login` first.');
23
- const meta = await fetchGitHubJson(`/repos/${pr.owner}/${pr.repo}/pulls/${pr.prNumber}`, token);
24
- return { pr, token, headSha: meta.head.sha, ref: `${pr.owner}/${pr.repo}#${pr.prNumber}` };
25
- }
26
- export async function fetchTracesListPayload(ref) {
27
- const { pr, token, headSha, ref: normalizedRef } = await tokenAndHead(ref);
28
- const overview = await fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token);
29
- return withSchema('traces', {
30
- operation: 'list',
31
- ref: normalizedRef,
32
- head_sha: headSha,
33
- checkpoints: (overview?.traces ?? []).map(publicCheckpoint),
34
- });
35
- }
36
- export async function tracesListCommand(ref, options) {
37
- const payload = await fetchTracesListPayload(ref);
38
- if (options.json) {
39
- console.log(JSON.stringify(payload, null, 2));
40
- return;
41
- }
42
- if (payload.checkpoints.length === 0) {
43
- console.log(chalk.dim('\nNo retained customer traces for this PR.\n'));
44
- return;
45
- }
46
- console.log(chalk.bold('\nCustomer traces\n'));
47
- for (const trace of payload.checkpoints) {
48
- console.log(`${chalk.cyan(trace.checkpoint_id)} ${trace.commit_message}`);
49
- console.log(` ${trace.summary}\n`);
50
- }
51
- }
52
- export async function fetchTracesGetPayload(ref, checkpointId, options) {
53
- const { pr, token, headSha, ref: normalizedRef } = await tokenAndHead(ref);
54
- const [trace, overview] = await Promise.all([
55
- fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, `entire_trace_chunks/${encodeURIComponent(checkpointId)}.json`, token),
56
- fetchAnalysisArtifact(pr.owner, pr.repo, pr.prNumber, 'entire_traces.json', token),
57
- ]);
58
- if (!trace)
59
- throw new Error(`Trace checkpoint not found: ${checkpointId}`);
60
- if (trace.checkpointId !== checkpointId)
61
- throw new Error('Trace checkpoint provenance mismatch');
62
- const cursor = Math.max(0, Number.parseInt(options.cursor ?? '0', 10) || 0);
63
- const limit = Math.min(100, Math.max(1, Number.parseInt(options.limit ?? '20', 10) || 20));
64
- const chunks = trace.chunks.slice(cursor, cursor + limit);
65
- const next = cursor + chunks.length < trace.chunks.length ? String(cursor + chunks.length) : null;
66
- return withSchema('traces', {
67
- operation: 'get',
68
- ref: normalizedRef,
69
- head_sha: headSha,
70
- checkpoint_id: trace.checkpointId,
71
- checkpoint: (() => {
72
- const item = overview?.traces?.find((candidate) => candidate.checkpointId === trace.checkpointId);
73
- return item ? publicCheckpoint(item) : null;
74
- })(),
75
- chunks,
76
- total_chunks: trace.chunks.length,
77
- total_chars: trace.totalChars,
78
- next_cursor: next,
79
- });
80
- }
81
- export async function tracesGetCommand(ref, checkpointId, options) {
82
- const payload = await fetchTracesGetPayload(ref, checkpointId, options);
83
- const chunks = payload.chunks;
84
- const next = payload.next_cursor;
85
- if (options.json) {
86
- console.log(JSON.stringify(payload, null, 2));
87
- return;
88
- }
89
- console.log(chunks.join('\n\n---\n\n'));
90
- if (next)
91
- console.log(chalk.dim(`\nMore available: --cursor ${next}\n`));
92
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Local Entire CLI trace discovery for intent drift analysis.
3
- *
4
- * Finds .entire/metadata/ session files whose modified files
5
- * overlap with the current diff's changed files.
6
- */
7
- /**
8
- * Find local Entire CLI trace files relevant to the current diff.
9
- *
10
- * Scans .entire/metadata/ for session directories containing full.jsonl,
11
- * then checks if any files modified in those sessions overlap with the
12
- * files changed in the current diff.
13
- *
14
- * Returns paths to condensed files containing only user messages (not the
15
- * raw multi-MB traces). This keeps the intent-drift sub-agent focused on
16
- * user intent without burning its context window on tool outputs.
17
- *
18
- * @returns Array of absolute paths to condensed user-messages files
19
- */
20
- export declare function findRelevantTraces(gitRoot: string, baseBranch: string): string[];