@haystackeditor/cli 0.10.3 → 0.12.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 (42) hide show
  1. package/README.md +15 -16
  2. package/dist/assets/hooks/agent-context/detect.ts +180 -0
  3. package/dist/assets/hooks/agent-context/format.ts +1 -0
  4. package/dist/assets/hooks/agent-context/index.ts +2 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +14 -5
  6. package/dist/assets/hooks/agent-context/parsers/codex.ts +416 -0
  7. package/dist/assets/hooks/agent-context/tsconfig.json +1 -0
  8. package/dist/assets/hooks/agent-context/types.ts +1 -1
  9. package/dist/assets/hooks/package.json +2 -1
  10. package/dist/assets/hooks/scripts/pre-commit.sh +80 -2
  11. package/dist/assets/hooks/scripts/pre-push.sh +1 -1
  12. package/dist/assets/skills/submit.md +20 -0
  13. package/dist/commands/config.d.ts +4 -0
  14. package/dist/commands/config.js +94 -69
  15. package/dist/commands/dismiss.js +2 -1
  16. package/dist/commands/install-session-hooks.d.ts +3 -2
  17. package/dist/commands/install-session-hooks.js +27 -11
  18. package/dist/commands/pr-status.d.ts +5 -0
  19. package/dist/commands/pr-status.js +2 -2
  20. package/dist/commands/request-review.d.ts +15 -0
  21. package/dist/commands/request-review.js +95 -0
  22. package/dist/commands/setup.d.ts +1 -0
  23. package/dist/commands/setup.js +285 -2
  24. package/dist/commands/submit.d.ts +1 -0
  25. package/dist/commands/submit.js +12 -19
  26. package/dist/commands/triage.d.ts +16 -4
  27. package/dist/commands/triage.js +278 -179
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.js +73 -50
  30. package/dist/triage/prompts.js +13 -5
  31. package/dist/triage/runner.js +1 -1
  32. package/dist/triage/traces.js +9 -3
  33. package/dist/triage/types.d.ts +1 -1
  34. package/dist/types.d.ts +146 -26
  35. package/dist/types.js +44 -5
  36. package/dist/utils/analysis-api.d.ts +16 -0
  37. package/dist/utils/analysis-api.js +23 -5
  38. package/dist/utils/telemetry.d.ts +17 -0
  39. package/dist/utils/telemetry.js +109 -0
  40. package/package.json +1 -1
  41. package/dist/commands/check-pending.d.ts +0 -19
  42. package/dist/commands/check-pending.js +0 -217
package/dist/types.js CHANGED
@@ -94,8 +94,31 @@ export const AI_REVIEWER_DISPLAY_NAMES = {
94
94
  codereviewer: 'CodeReviewBot',
95
95
  codium: 'Codium',
96
96
  };
97
+ /** Maps friendly source names to exact GitHub bot login usernames. */
98
+ export const AI_REVIEWER_BOT_USERNAMES = {
99
+ cursor: 'cursor-bugbot[bot]',
100
+ coderabbit: 'coderabbitai[bot]',
101
+ copilot: 'copilot[bot]',
102
+ greptile: 'greptile[bot]',
103
+ sourcery: 'sourcery-ai[bot]',
104
+ qodo: 'qodo-merge-pro[bot]',
105
+ codeant: 'codeant-ai[bot]',
106
+ ellipsis: 'ellipsis-dev[bot]',
107
+ korbit: 'korbit-ai[bot]',
108
+ panto: 'pantoaibot[bot]',
109
+ bito: 'bito[bot]',
110
+ graphite: 'graphite-app[bot]',
111
+ deepsource: 'deepsource-io[bot]',
112
+ snyk: 'snyk[bot]',
113
+ codacy: 'codacy-production[bot]',
114
+ sonarcloud: 'sonarcloud[bot]',
115
+ sweep: 'sweep-ai[bot]',
116
+ codeguru: 'aws-codeguru-reviewer[bot]',
117
+ codereviewer: 'codereviewbot[bot]',
118
+ codium: 'codiumai[bot]',
119
+ };
97
120
  const AIReviewerSourceSchema = z.enum(AI_REVIEWER_SOURCES);
98
- const BabysitterConfigSchema = z.object({
121
+ const MergeQueueConfigSchema = z.object({
99
122
  /** Enable the Haystack auto-merge queue */
100
123
  merge_queue: z.boolean().optional(),
101
124
  /** Automatically rebase PRs that develop merge conflicts after pushes to the base branch */
@@ -107,8 +130,24 @@ const BabysitterConfigSchema = z.object({
107
130
  /** Per-author wait time overrides (author login → minutes) */
108
131
  authors: z.record(z.string(), z.number()).optional(),
109
132
  }).optional(),
110
- /** AI reviewer sources to wait for before starting the merge grace period */
111
- expected_ai_reviewers: z.array(AIReviewerSourceSchema).optional(),
133
+ /** Batch multiple PRs into one CI run */
134
+ batch_merge: z.object({
135
+ /** Enable batch merging */
136
+ enabled: z.boolean().optional(),
137
+ /** Minimum PRs to form a batch */
138
+ min_batch_size: z.number().optional(),
139
+ /** Maximum PRs per batch (cap: 20) */
140
+ max_batch_size: z.number().optional(),
141
+ /** Seconds to wait after grace period for more PRs to accumulate (default: 30, 0 to disable) */
142
+ batch_window_seconds: z.number().optional(),
143
+ }).optional(),
144
+ /** Wait for external AI code reviewers before merging — all bots must post */
145
+ wait_for_reviewer: z.object({
146
+ /** GitHub bot usernames to wait for (e.g. "cursor-bugbot[bot]") */
147
+ bots: z.array(z.string()),
148
+ /** Give up waiting after this many minutes (default: 30) */
149
+ timeout_minutes: z.number().optional(),
150
+ }).optional(),
112
151
  });
113
152
  // =============================================================================
114
153
  // SERVICE (for monorepos)
@@ -253,6 +292,6 @@ export const HaystackConfigSchema = z.object({
253
292
  network: NetworkConfigSchema.optional(),
254
293
  /** Secrets required by this project */
255
294
  secrets: z.record(z.string(), SecretDeclarationSchema).optional(),
256
- /** Babysitter configuration for autonomous PR maintenance */
257
- babysitter: BabysitterConfigSchema.optional(),
295
+ /** Merge queue configuration for autonomous PR maintenance */
296
+ merge_queue: MergeQueueConfigSchema.optional(),
258
297
  });
@@ -105,4 +105,20 @@ export declare function fetchAnalysisResults(owner: string, repo: string, prNumb
105
105
  * This is the rich analysis data shown on the Haystack feed — includes
106
106
  * verdict, rating, structured findings, verified bugs, and agent fix prompts.
107
107
  */
108
+ export interface AutoFixManifest {
109
+ jobId: string;
110
+ repo: string;
111
+ prNumber: number;
112
+ fixedItems: Array<{
113
+ category: string;
114
+ summary: string;
115
+ }>;
116
+ leftForReview: Array<{
117
+ category: string;
118
+ summary: string;
119
+ reason: string;
120
+ }>;
121
+ timestamp: string;
122
+ }
123
+ export declare function fetchAutoFixManifest(owner: string, repo: string, prNumber: number, token?: string): Promise<AutoFixManifest | null>;
108
124
  export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
@@ -160,11 +160,29 @@ export async function fetchAnalysisResults(owner, repo, prNumber, token) {
160
160
  autoMerged,
161
161
  };
162
162
  }
163
- /**
164
- * Fetch the full rating synthesis for a PR.
165
- * This is the rich analysis data shown on the Haystack feed — includes
166
- * verdict, rating, structured findings, verified bugs, and agent fix prompts.
167
- */
163
+ export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
164
+ const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}/auto-fix-manifest.json`;
165
+ try {
166
+ const response = await fetch(url, {
167
+ headers: apiHeaders(token),
168
+ });
169
+ if (!response.ok)
170
+ return null;
171
+ const data = await response.json();
172
+ if (data.download_url) {
173
+ const s3Resp = await fetch(data.download_url, {
174
+ headers: { 'User-Agent': 'Haystack-CLI' },
175
+ });
176
+ if (!s3Resp.ok)
177
+ return null;
178
+ return await s3Resp.json();
179
+ }
180
+ return data;
181
+ }
182
+ catch {
183
+ return null;
184
+ }
185
+ }
168
186
  export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
169
187
  // Use v3/result endpoint (same as the Lambda serves generic files)
170
188
  const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}/rating_synthesis.json`;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Lightweight CLI telemetry — fire-and-forget error tracking via PostHog HTTP API.
3
+ *
4
+ * Events go to the same PostHog project as the web app, enabling unified
5
+ * alerting across frontend, CLI, and backend.
6
+ *
7
+ * All calls are non-blocking and never throw — telemetry failures must not
8
+ * break CLI commands.
9
+ */
10
+ /**
11
+ * Track an error event. Fire-and-forget — never throws.
12
+ */
13
+ export declare function trackError(errorType: string, metadata?: Record<string, unknown>): void;
14
+ /**
15
+ * Track a setup event (non-error). Fire-and-forget.
16
+ */
17
+ export declare function trackSetupEvent(step: string, metadata?: Record<string, unknown>): void;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Lightweight CLI telemetry — fire-and-forget error tracking via PostHog HTTP API.
3
+ *
4
+ * Events go to the same PostHog project as the web app, enabling unified
5
+ * alerting across frontend, CLI, and backend.
6
+ *
7
+ * All calls are non-blocking and never throw — telemetry failures must not
8
+ * break CLI commands.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { dirname, resolve } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import * as os from 'os';
14
+ const POSTHOG_HOST = 'https://us.i.posthog.com';
15
+ const POSTHOG_API_KEY = process.env.HAYSTACK_POSTHOG_KEY || 'phc_7wHrCoMlcwuftLe6N38hPb9wKHb4OK0wNw9ayeRySad';
16
+ const CURRENT_FILE_DIR = dirname(fileURLToPath(import.meta.url));
17
+ const PACKAGE_JSON_PATH = resolve(CURRENT_FILE_DIR, '../../package.json');
18
+ /**
19
+ * Stable machine identifier for PostHog distinct_id.
20
+ * Uses hostname + username so events are attributable per-machine.
21
+ * Never throws — falls back to hostname-only or 'anonymous'.
22
+ */
23
+ function getDistinctId() {
24
+ try {
25
+ const hostname = os.hostname();
26
+ try {
27
+ return `cli:${os.userInfo().username}@${hostname}`;
28
+ }
29
+ catch {
30
+ return `cli:${hostname}`;
31
+ }
32
+ }
33
+ catch {
34
+ return 'cli:anonymous';
35
+ }
36
+ }
37
+ /**
38
+ * Track an error event. Fire-and-forget — never throws.
39
+ */
40
+ export function trackError(errorType, metadata) {
41
+ try {
42
+ if (typeof globalThis.fetch !== 'function')
43
+ return;
44
+ const event = {
45
+ api_key: POSTHOG_API_KEY,
46
+ distinct_id: getDistinctId(),
47
+ event: 'cli_error',
48
+ properties: {
49
+ error_type: errorType,
50
+ cli_version: getCLIVersion(),
51
+ os: process.platform,
52
+ arch: process.arch,
53
+ node_version: process.version,
54
+ timestamp: new Date().toISOString(),
55
+ ...metadata,
56
+ },
57
+ timestamp: new Date().toISOString(),
58
+ };
59
+ // Fire-and-forget — never block the CLI
60
+ globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify(event),
64
+ }).catch(() => { });
65
+ }
66
+ catch {
67
+ // Telemetry must never crash the CLI
68
+ }
69
+ }
70
+ /**
71
+ * Track a setup event (non-error). Fire-and-forget.
72
+ */
73
+ export function trackSetupEvent(step, metadata) {
74
+ try {
75
+ if (typeof globalThis.fetch !== 'function')
76
+ return;
77
+ const event = {
78
+ api_key: POSTHOG_API_KEY,
79
+ distinct_id: getDistinctId(),
80
+ event: 'cli_setup',
81
+ properties: {
82
+ step,
83
+ cli_version: getCLIVersion(),
84
+ os: process.platform,
85
+ arch: process.arch,
86
+ timestamp: new Date().toISOString(),
87
+ ...metadata,
88
+ },
89
+ timestamp: new Date().toISOString(),
90
+ };
91
+ globalThis.fetch(`${POSTHOG_HOST}/capture/`, {
92
+ method: 'POST',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify(event),
95
+ }).catch(() => { });
96
+ }
97
+ catch {
98
+ // Telemetry must never crash the CLI
99
+ }
100
+ }
101
+ function getCLIVersion() {
102
+ try {
103
+ const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
104
+ return typeof packageJson.version === 'string' ? packageJson.version : 'unknown';
105
+ }
106
+ catch {
107
+ return 'unknown';
108
+ }
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.10.3",
3
+ "version": "0.12.0",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,19 +0,0 @@
1
- /**
2
- * check-pending command — Check status of pending Haystack analysis.
3
- *
4
- * Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
5
- * and outputs a two-state verdict:
6
- * - "Good to merge" (no issues)
7
- * - "Needs your input" (issues found)
8
- *
9
- * Output modes:
10
- * --hook Minimal single-line for session-start hooks
11
- * --json Machine-readable JSON
12
- * (default) Rich terminal output
13
- */
14
- export interface CheckPendingOptions {
15
- json?: boolean;
16
- hook?: boolean;
17
- clear?: boolean;
18
- }
19
- export declare function checkPendingCommand(options: CheckPendingOptions): Promise<void>;
@@ -1,217 +0,0 @@
1
- /**
2
- * check-pending command — Check status of pending Haystack analysis.
3
- *
4
- * Reads `.haystack/pending-submit.json`, polls the analysis API if needed,
5
- * and outputs a two-state verdict:
6
- * - "Good to merge" (no issues)
7
- * - "Needs your input" (issues found)
8
- *
9
- * Output modes:
10
- * --hook Minimal single-line for session-start hooks
11
- * --json Machine-readable JSON
12
- * (default) Rich terminal output
13
- */
14
- import chalk from 'chalk';
15
- import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
16
- import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
17
- import { findExistingPR } from '../utils/github-api.js';
18
- import { loadToken } from './login.js';
19
- // ============================================================================
20
- // Polling
21
- // ============================================================================
22
- const POLL_INTERVAL_MS = 3_000;
23
- const POLL_TIMEOUT_MS = 30_000;
24
- /**
25
- * Poll the result endpoint until analysis is ready or timeout.
26
- */
27
- async function waitForAnalysis(owner, repo, prNumber, timeoutMs, token) {
28
- const deadline = Date.now() + timeoutMs;
29
- let consecutiveErrors = 0;
30
- while (Date.now() < deadline) {
31
- const result = await checkAnalysisReady(owner, repo, prNumber, token);
32
- if (result.status === 'ready')
33
- return 'completed';
34
- if (result.status === 'error') {
35
- consecutiveErrors++;
36
- if (consecutiveErrors >= 5)
37
- return 'failed';
38
- }
39
- else {
40
- consecutiveErrors = 0;
41
- }
42
- await new Promise(r => setTimeout(r, POLL_INTERVAL_MS));
43
- }
44
- return 'pending';
45
- }
46
- // ============================================================================
47
- // Output formatting
48
- // ============================================================================
49
- function printHookOutput(pending) {
50
- if (!pending.analysisResult) {
51
- console.log(`[Haystack] PR #${pending.prNumber}: Analysis in progress...`);
52
- return;
53
- }
54
- const result = pending.analysisResult;
55
- if (result.state === 'good-to-merge') {
56
- if (result.autoMerged) {
57
- console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Auto-merged`);
58
- }
59
- else {
60
- console.log(`[Haystack] \u2705 PR #${pending.prNumber} "${getBranchLabel(pending)}": Good to merge`);
61
- }
62
- }
63
- else {
64
- const parts = [];
65
- if (result.bugCount > 0)
66
- parts.push(`${result.bugCount} bug(s)`);
67
- if (result.ruleViolationCount > 0)
68
- parts.push(`${result.ruleViolationCount} rule violation(s)`);
69
- if (parts.length === 0)
70
- parts.push(`${result.warningCount} warning(s)`);
71
- console.log(`[Haystack] \u26A0\uFE0F PR #${pending.prNumber} "${getBranchLabel(pending)}": Needs your input (${parts.join(', ')})`);
72
- console.log(` Review: ${result.reviewUrl}`);
73
- }
74
- }
75
- function printRichOutput(pending) {
76
- console.log(chalk.bold('\n' + '\u2501'.repeat(50)));
77
- console.log(chalk.bold(` Haystack Analysis: PR #${pending.prNumber}`));
78
- console.log(chalk.bold('\u2501'.repeat(50) + '\n'));
79
- console.log(` ${chalk.dim('Branch:')} ${pending.branch} \u2192 ${pending.baseBranch}`);
80
- console.log(` ${chalk.dim('URL:')} ${chalk.cyan(pending.prUrl)}`);
81
- if (!pending.analysisResult) {
82
- if (pending.status === 'polling') {
83
- console.log(` ${chalk.dim('Status:')} ${chalk.yellow('Analysis in progress...')}`);
84
- }
85
- else if (pending.status === 'failed') {
86
- console.log(` ${chalk.dim('Status:')} ${chalk.red('Analysis failed')}`);
87
- }
88
- else if (pending.status === 'stale') {
89
- console.log(` ${chalk.dim('Status:')} ${chalk.dim('Stale (submitted > 2h ago)')}`);
90
- }
91
- console.log('');
92
- return;
93
- }
94
- const result = pending.analysisResult;
95
- if (result.state === 'good-to-merge') {
96
- if (result.autoMerged) {
97
- console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Auto-merged')}`);
98
- console.log(` ${chalk.dim('Issues:')} ${chalk.green('None — PR was automatically merged')}`);
99
- }
100
- else {
101
- console.log(` ${chalk.dim('Verdict:')} ${chalk.green.bold('Good to merge')}`);
102
- console.log(` ${chalk.dim('Issues:')} ${chalk.green('None')}`);
103
- }
104
- }
105
- else {
106
- console.log(` ${chalk.dim('Verdict:')} ${chalk.yellow.bold('Needs your input')}`);
107
- console.log(` ${chalk.dim('Issues:')} ${result.summary}`);
108
- console.log('');
109
- for (const issue of result.issues) {
110
- const icon = issue.severity === 'error' ? chalk.red('\u2717') : chalk.yellow('!');
111
- const loc = issue.line ? `${issue.file}:${issue.line}` : issue.file;
112
- console.log(` ${icon} ${chalk.dim(loc)} ${issue.message}`);
113
- }
114
- }
115
- console.log(`\n ${chalk.dim('Review:')} ${chalk.cyan(result.reviewUrl)}`);
116
- console.log('');
117
- }
118
- function printJsonOutput(pending) {
119
- const output = {
120
- prNumber: pending.prNumber,
121
- prUrl: pending.prUrl,
122
- branch: pending.branch,
123
- baseBranch: pending.baseBranch,
124
- status: pending.status,
125
- submittedAt: pending.submittedAt,
126
- verdict: pending.analysisResult?.state || null,
127
- bugCount: pending.analysisResult?.bugCount || 0,
128
- warningCount: pending.analysisResult?.warningCount || 0,
129
- ruleViolationCount: pending.analysisResult?.ruleViolationCount || 0,
130
- summary: pending.analysisResult?.summary || null,
131
- issues: pending.analysisResult?.issues || [],
132
- reviewUrl: pending.analysisResult?.reviewUrl || null,
133
- autoMerged: pending.analysisResult?.autoMerged || false,
134
- };
135
- console.log(JSON.stringify(output, null, 2));
136
- }
137
- function getBranchLabel(pending) {
138
- return pending.branch.replace(/^refs\/heads\//, '');
139
- }
140
- // ============================================================================
141
- // Main command
142
- // ============================================================================
143
- export async function checkPendingCommand(options) {
144
- // Handle --clear
145
- if (options.clear) {
146
- clearPendingSubmit();
147
- if (!options.hook) {
148
- console.log(chalk.dim('Pending submit state cleared.'));
149
- }
150
- return;
151
- }
152
- // Load pending state
153
- const pending = loadPendingSubmit();
154
- if (!pending) {
155
- if (options.json) {
156
- console.log(JSON.stringify({ status: 'none' }));
157
- }
158
- else if (!options.hook) {
159
- // Silent for hook mode — no pending submits is fine
160
- console.log(chalk.dim('No pending submits.\n'));
161
- }
162
- return;
163
- }
164
- // Load token for private repo auth (best-effort, don't block if unavailable)
165
- const token = await loadToken() || undefined;
166
- // If still polling, try to resolve it
167
- if (pending.status === 'polling') {
168
- // Quick check: is the PR still open?
169
- try {
170
- const pr = await findExistingPR(pending.owner, pending.repo, pending.branch);
171
- if (!pr) {
172
- // PR was closed or merged
173
- updatePendingSubmit({ status: 'stale', resolvedAt: new Date().toISOString() });
174
- pending.status = 'stale';
175
- }
176
- }
177
- catch {
178
- // Auth not available or API error — continue with polling
179
- }
180
- if (pending.status === 'polling') {
181
- // For hook mode, do a single quick poll (no waiting)
182
- const timeout = options.hook ? 5_000 : POLL_TIMEOUT_MS;
183
- const result = await waitForAnalysis(pending.owner, pending.repo, pending.prNumber, timeout, token);
184
- if (result === 'completed') {
185
- // Fetch the actual analysis results
186
- try {
187
- const verdict = await fetchAnalysisResults(pending.owner, pending.repo, pending.prNumber, token);
188
- updatePendingSubmit({
189
- status: 'completed',
190
- analysisResult: verdict,
191
- resolvedAt: new Date().toISOString(),
192
- });
193
- pending.status = 'completed';
194
- pending.analysisResult = verdict;
195
- }
196
- catch {
197
- // Could not fetch results — leave as polling
198
- }
199
- }
200
- else if (result === 'failed') {
201
- updatePendingSubmit({ status: 'failed', resolvedAt: new Date().toISOString() });
202
- pending.status = 'failed';
203
- }
204
- // 'pending' — leave as-is for next check
205
- }
206
- }
207
- // Output based on mode
208
- if (options.json) {
209
- printJsonOutput(pending);
210
- }
211
- else if (options.hook) {
212
- printHookOutput(pending);
213
- }
214
- else {
215
- printRichOutput(pending);
216
- }
217
- }