@haystackeditor/cli 0.8.0 → 0.9.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 (76) hide show
  1. package/README.md +105 -12
  2. package/dist/assets/hooks/agent-context/detect.ts +136 -0
  3. package/dist/assets/hooks/agent-context/format.ts +99 -0
  4. package/dist/assets/hooks/agent-context/index.ts +39 -0
  5. package/dist/assets/hooks/agent-context/parsers/claude.ts +253 -0
  6. package/dist/assets/hooks/agent-context/parsers/gemini.ts +155 -0
  7. package/dist/assets/hooks/agent-context/parsers/opencode.ts +174 -0
  8. package/dist/assets/hooks/agent-context/tsconfig.json +13 -0
  9. package/dist/assets/hooks/agent-context/types.ts +58 -0
  10. package/dist/assets/hooks/llm-rules-template.md +56 -0
  11. package/dist/assets/hooks/package.json +11 -0
  12. package/dist/assets/hooks/scripts/commit-msg.sh +4 -0
  13. package/dist/assets/hooks/scripts/post-commit.sh +4 -0
  14. package/dist/assets/hooks/scripts/pre-commit.sh +92 -0
  15. package/dist/assets/hooks/scripts/pre-push.sh +25 -0
  16. package/dist/assets/hooks/scripts/prepare-commit-msg.sh +3 -0
  17. package/dist/assets/hooks/truncation-checker/ast-analyzer.ts +528 -0
  18. package/dist/assets/hooks/truncation-checker/index.ts +595 -0
  19. package/dist/assets/hooks/truncation-checker/tsconfig.json +13 -0
  20. package/dist/assets/skills/prepare-haystack.md +323 -0
  21. package/dist/assets/skills/secrets.md +164 -0
  22. package/dist/assets/skills/setup-external-sandbox.md +243 -0
  23. package/dist/assets/skills/setup-haystack.md +639 -0
  24. package/dist/assets/skills/submit.md +154 -0
  25. package/dist/assets/templates/CLAUDE.md.snippet +42 -0
  26. package/dist/assets/templates/haystack.yml +193 -0
  27. package/dist/commands/check-pending.d.ts +19 -0
  28. package/dist/commands/check-pending.js +217 -0
  29. package/dist/commands/config.d.ts +18 -12
  30. package/dist/commands/config.js +327 -52
  31. package/dist/commands/hooks.d.ts +17 -0
  32. package/dist/commands/hooks.js +269 -0
  33. package/dist/commands/install-session-hooks.d.ts +16 -0
  34. package/dist/commands/install-session-hooks.js +302 -0
  35. package/dist/commands/login.js +1 -1
  36. package/dist/commands/policy.d.ts +31 -0
  37. package/dist/commands/policy.js +365 -0
  38. package/dist/commands/skills.d.ts +8 -0
  39. package/dist/commands/skills.js +80 -0
  40. package/dist/commands/submit.d.ts +22 -0
  41. package/dist/commands/submit.js +428 -0
  42. package/dist/commands/triage.d.ts +16 -0
  43. package/dist/commands/triage.js +354 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +317 -2
  46. package/dist/tools/detect.d.ts +50 -0
  47. package/dist/tools/detect.js +853 -0
  48. package/dist/tools/fixtures.d.ts +38 -0
  49. package/dist/tools/fixtures.js +199 -0
  50. package/dist/tools/setup.d.ts +43 -0
  51. package/dist/tools/setup.js +597 -0
  52. package/dist/triage/prompts.d.ts +31 -0
  53. package/dist/triage/prompts.js +266 -0
  54. package/dist/triage/runner.d.ts +21 -0
  55. package/dist/triage/runner.js +325 -0
  56. package/dist/triage/traces.d.ts +20 -0
  57. package/dist/triage/traces.js +305 -0
  58. package/dist/triage/types.d.ts +47 -0
  59. package/dist/triage/types.js +7 -0
  60. package/dist/types.d.ts +1387 -191
  61. package/dist/types.js +254 -2
  62. package/dist/utils/analysis-api.d.ts +108 -0
  63. package/dist/utils/analysis-api.js +194 -0
  64. package/dist/utils/config.js +1 -1
  65. package/dist/utils/git.d.ts +80 -0
  66. package/dist/utils/git.js +302 -0
  67. package/dist/utils/github-api.d.ts +83 -0
  68. package/dist/utils/github-api.js +266 -0
  69. package/dist/utils/hooks.d.ts +26 -0
  70. package/dist/utils/hooks.js +226 -0
  71. package/dist/utils/pending-state.d.ts +38 -0
  72. package/dist/utils/pending-state.js +86 -0
  73. package/dist/utils/secrets.js +3 -3
  74. package/dist/utils/skill.d.ts +1 -1
  75. package/dist/utils/skill.js +658 -1
  76. package/package.json +5 -3
package/dist/types.js CHANGED
@@ -1,6 +1,258 @@
1
1
  /**
2
2
  * Haystack CLI Types
3
3
  *
4
- * These types define the .haystack.json schema that sandbox.py reads.
4
+ * Zod schemas for .haystack.json configuration + CLI-only types.
5
+ * The config schema matches what agent/modal/sandbox.py expects to read.
5
6
  */
6
- export {};
7
+ import { z } from 'zod';
8
+ // =============================================================================
9
+ // VERIFICATION COMMANDS
10
+ // =============================================================================
11
+ const VerificationCommandSchema = z.object({
12
+ /** Human-readable name (e.g., "Build", "Lint", "Typecheck") */
13
+ name: z.string(),
14
+ /** The shell command to run (e.g., "pnpm build", "pnpm lint") */
15
+ run: z.string(),
16
+ });
17
+ // =============================================================================
18
+ // FIXTURE SOURCES
19
+ // =============================================================================
20
+ const FixtureEntrySchema = z.object({
21
+ /** Where to load fixture data from */
22
+ source: z.string().refine((val) => val === 'passthrough' ||
23
+ val.startsWith('file://') ||
24
+ val.startsWith('https://') ||
25
+ val.startsWith('http://') ||
26
+ val.startsWith('s3://') ||
27
+ val.startsWith('r2://') ||
28
+ val.startsWith('script://'), {
29
+ message: 'Source must be passthrough, or start with file://, https://, http://, s3://, r2://, or script://',
30
+ }),
31
+ /** Headers to include when fetching from remote source */
32
+ headers: z.record(z.string(), z.string()).optional(),
33
+ /** Cookies to include when fetching from remote source */
34
+ cookies: z.string().optional(),
35
+ });
36
+ // =============================================================================
37
+ // DEV SERVER
38
+ // =============================================================================
39
+ const DevServerSchema = z.object({
40
+ /** Command to start dev server (e.g., "pnpm dev") */
41
+ command: z.string(),
42
+ /** Port the dev server runs on */
43
+ port: z.number(),
44
+ /** Pattern in stdout that indicates server is ready (e.g., "Local:") */
45
+ ready_pattern: z.string().optional(),
46
+ /** Environment variables to set when starting the server */
47
+ env: z.record(z.string(), z.string()).optional(),
48
+ });
49
+ // =============================================================================
50
+ // BABYSITTER CONFIGURATION
51
+ // =============================================================================
52
+ /** Valid AI reviewer source identifiers that the merge queue can wait for */
53
+ export const AI_REVIEWER_SOURCES = [
54
+ 'cursor',
55
+ 'coderabbit',
56
+ 'copilot',
57
+ 'greptile',
58
+ 'sourcery',
59
+ 'qodo',
60
+ 'codeant',
61
+ 'ellipsis',
62
+ 'korbit',
63
+ 'panto',
64
+ 'bito',
65
+ 'graphite',
66
+ 'deepsource',
67
+ 'snyk',
68
+ 'codacy',
69
+ 'sonarcloud',
70
+ 'sweep',
71
+ 'codeguru',
72
+ 'codereviewer',
73
+ 'codium',
74
+ ];
75
+ export const AI_REVIEWER_DISPLAY_NAMES = {
76
+ cursor: 'Cursor BugBot',
77
+ coderabbit: 'CodeRabbit',
78
+ copilot: 'GitHub Copilot',
79
+ greptile: 'Greptile',
80
+ sourcery: 'Sourcery',
81
+ qodo: 'Qodo',
82
+ codeant: 'CodeAnt AI',
83
+ ellipsis: 'Ellipsis',
84
+ korbit: 'Korbit AI',
85
+ panto: 'Panto AI',
86
+ bito: 'Bito AI',
87
+ graphite: 'Graphite',
88
+ deepsource: 'DeepSource',
89
+ snyk: 'Snyk',
90
+ codacy: 'Codacy',
91
+ sonarcloud: 'SonarCloud',
92
+ sweep: 'Sweep AI',
93
+ codeguru: 'Amazon CodeGuru',
94
+ codereviewer: 'CodeReviewBot',
95
+ codium: 'Codium',
96
+ };
97
+ const AIReviewerSourceSchema = z.enum(AI_REVIEWER_SOURCES);
98
+ const BabysitterConfigSchema = z.object({
99
+ /** Enable the Haystack auto-merge queue */
100
+ merge_queue: z.boolean().optional(),
101
+ /** Automatically rebase PRs that develop merge conflicts after pushes to the base branch */
102
+ auto_resolve_conflicts: z.boolean().optional(),
103
+ /** Grace period configuration before auto-merging */
104
+ merge_debounce: z.object({
105
+ /** Default wait time in minutes before auto-merging */
106
+ default_minutes: z.number().optional(),
107
+ /** Per-author wait time overrides (author login → minutes) */
108
+ authors: z.record(z.string(), z.number()).optional(),
109
+ }).optional(),
110
+ /** AI reviewer sources to wait for before starting the merge grace period */
111
+ expected_ai_reviewers: z.array(AIReviewerSourceSchema).optional(),
112
+ });
113
+ // =============================================================================
114
+ // SERVICE (for monorepos)
115
+ // =============================================================================
116
+ const ServiceSchema = z.object({
117
+ /** Root directory relative to repo root */
118
+ root: z.string().optional(),
119
+ /** Command to start the service */
120
+ command: z.string(),
121
+ /** Port the service runs on (not required for batch jobs) */
122
+ port: z.number().optional(),
123
+ /** Pattern in stdout that indicates service is ready */
124
+ ready_pattern: z.string().optional(),
125
+ /** Environment variables to set */
126
+ env: z.record(z.string(), z.string()).optional(),
127
+ /** Service type: 'server' (default) or 'batch' (run once, no port) */
128
+ type: z.enum(['server', 'batch']).optional(),
129
+ /** Services that must be started before this service */
130
+ depends_on: z.array(z.string()).optional(),
131
+ });
132
+ // =============================================================================
133
+ // VERIFICATION
134
+ // =============================================================================
135
+ const VerificationSchema = z.object({
136
+ /** Commands to run for verification (build, lint, test, etc.) */
137
+ commands: z.array(VerificationCommandSchema).optional(),
138
+ /** Fixture data sources keyed by endpoint pattern */
139
+ fixtures: z.record(z.string(), FixtureEntrySchema).optional(),
140
+ });
141
+ // =============================================================================
142
+ // AUTH CONFIGURATION
143
+ // =============================================================================
144
+ const AuthStrategySchema = z.enum(['bypass', 'fixture', 'token', 'test_account']);
145
+ const TokenInjectionSchema = z.object({
146
+ type: z.enum(['cookie', 'localStorage', 'sessionStorage', 'header']),
147
+ name: z.string(),
148
+ secret: z.string(),
149
+ cookie_options: z
150
+ .object({
151
+ path: z.string().optional(),
152
+ domain: z.string().optional(),
153
+ secure: z.boolean().optional(),
154
+ httpOnly: z.boolean().optional(),
155
+ sameSite: z.enum(['Strict', 'Lax', 'None']).optional(),
156
+ })
157
+ .optional(),
158
+ });
159
+ const LoginStepSchema = z.object({
160
+ action: z.enum(['navigate', 'type', 'click', 'wait', 'submit']),
161
+ url: z.string().optional(),
162
+ selector: z.string().optional(),
163
+ value: z.string().optional(),
164
+ timeout: z.number().optional(),
165
+ });
166
+ const TestAccountSchema = z.object({
167
+ credentials_secret: z.string(),
168
+ login_flow: z.array(LoginStepSchema),
169
+ success_selector: z.string().optional(),
170
+ failure_selector: z.string().optional(),
171
+ });
172
+ const AuthConfigSchema = z.object({
173
+ strategy: AuthStrategySchema.default('bypass'),
174
+ env: z.record(z.string(), z.string()).optional(),
175
+ fixture: z
176
+ .object({
177
+ pattern: z.string(),
178
+ source: z.string(),
179
+ })
180
+ .optional(),
181
+ token: TokenInjectionSchema.optional(),
182
+ test_account: TestAccountSchema.optional(),
183
+ fallback: AuthStrategySchema.optional(),
184
+ });
185
+ // =============================================================================
186
+ // DATABASE CONFIGURATION
187
+ // =============================================================================
188
+ const DatabaseStrategySchema = z.enum(['fixture', 'memory', 'seed', 'docker', 'remote']);
189
+ const DockerServiceSchema = z.object({
190
+ image: z.string(),
191
+ port: z.number(),
192
+ env: z.record(z.string(), z.string()).optional(),
193
+ health_check: z.string().optional(),
194
+ health_timeout: z.number().optional(),
195
+ });
196
+ const DatabaseConfigBaseSchema = z.object({
197
+ strategy: DatabaseStrategySchema.default('fixture'),
198
+ env: z.record(z.string(), z.string()).optional(),
199
+ seed_command: z.string().optional(),
200
+ migrate_command: z.string().optional(),
201
+ services: z.array(DockerServiceSchema).optional(),
202
+ read_only: z.boolean().optional(),
203
+ ready_timeout: z.number().optional(),
204
+ });
205
+ const DatabaseConfigSchema = DatabaseConfigBaseSchema.extend({
206
+ fallback: DatabaseConfigBaseSchema.optional(),
207
+ });
208
+ // =============================================================================
209
+ // AGENTIC CONFIGURATION
210
+ // =============================================================================
211
+ const AgenticToolSchema = z.enum(['opencode', 'claude-code', 'codex']);
212
+ const AgenticSchema = z.object({
213
+ tool: AgenticToolSchema.optional(),
214
+ });
215
+ // =============================================================================
216
+ // NETWORK CONFIGURATION
217
+ // =============================================================================
218
+ const NetworkConfigSchema = z.object({
219
+ /** Additional hosts/domains to allow egress to */
220
+ allow: z.array(z.string()).optional(),
221
+ });
222
+ // =============================================================================
223
+ // SECRET DECLARATION
224
+ // =============================================================================
225
+ const SecretDeclarationSchema = z.object({
226
+ description: z.string().optional(),
227
+ required: z.boolean().optional(),
228
+ services: z.array(z.string()).optional(),
229
+ });
230
+ // =============================================================================
231
+ // MAIN CONFIG
232
+ // =============================================================================
233
+ export const HaystackConfigSchema = z.object({
234
+ /** Config version (must be "1") */
235
+ version: z.literal('1'),
236
+ /** Project name (for display) */
237
+ name: z.string().optional(),
238
+ /** Package manager override (auto-detected if not specified) */
239
+ package_manager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
240
+ /** Development server configuration */
241
+ dev_server: DevServerSchema.optional(),
242
+ /** Multiple services (for monorepos) */
243
+ services: z.record(z.string(), ServiceSchema).optional(),
244
+ /** Verification configuration */
245
+ verification: VerificationSchema.optional(),
246
+ /** Auth configuration for sandbox verification */
247
+ auth: AuthConfigSchema.optional(),
248
+ /** Agentic configuration (project-level override) */
249
+ agentic: AgenticSchema.optional(),
250
+ /** Database configuration for sandbox verification */
251
+ database: DatabaseConfigSchema.optional(),
252
+ /** Network egress configuration */
253
+ network: NetworkConfigSchema.optional(),
254
+ /** Secrets required by this project */
255
+ secrets: z.record(z.string(), SecretDeclarationSchema).optional(),
256
+ /** Babysitter configuration for autonomous PR maintenance */
257
+ babysitter: BabysitterConfigSchema.optional(),
258
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Haystack Analysis API client.
3
+ *
4
+ * Polls the result endpoint by PR identifier to check if analysis is ready,
5
+ * then fetches detailed results (bug detection + rule violations).
6
+ *
7
+ * Analysis is triggered automatically by the GitHub App webhook when a PR
8
+ * is created or updated — the CLI does not need to trigger it directly.
9
+ */
10
+ export interface AnalysisVerdict {
11
+ state: 'good-to-merge' | 'needs-input' | 'failed';
12
+ bugCount: number;
13
+ warningCount: number;
14
+ ruleViolationCount: number;
15
+ summary: string;
16
+ issues: AnalysisIssue[];
17
+ reviewUrl: string;
18
+ /** Whether the PR was auto-merged after analysis */
19
+ autoMerged?: boolean;
20
+ }
21
+ export interface AnalysisIssue {
22
+ file: string;
23
+ line?: number;
24
+ message: string;
25
+ severity: 'error' | 'warning' | 'info';
26
+ source: 'bug-detection' | 'rule-violation';
27
+ }
28
+ export interface SynthesisDisplayIssue {
29
+ category: string;
30
+ summary: string;
31
+ detail: string;
32
+ agentFixPrompt?: string;
33
+ source?: string;
34
+ }
35
+ export interface VerifiedBug {
36
+ originalTitle: string;
37
+ verified: boolean;
38
+ assessedSeverity: 'low' | 'medium' | 'high' | 'critical';
39
+ likelihood?: 'rare' | 'uncommon' | 'common';
40
+ rationale: string;
41
+ source?: {
42
+ type: string;
43
+ commentId?: number;
44
+ author?: string;
45
+ };
46
+ }
47
+ export interface HumanReviewReason {
48
+ type: string;
49
+ severity: string;
50
+ reason: string;
51
+ source?: string;
52
+ }
53
+ export interface RatingSynthesis {
54
+ haystackRating: 1 | 2 | 3 | 4 | 5;
55
+ analysisVerdict?: 'clean' | 'has-issues';
56
+ synthesis: string;
57
+ synthesisDisplay?: SynthesisDisplayIssue[];
58
+ verifiedBugs: VerifiedBug[];
59
+ reviewRecommendation: {
60
+ canAutoMerge: boolean;
61
+ };
62
+ needsHumanReview?: boolean;
63
+ humanReviewReasons?: HumanReviewReason[];
64
+ phase?: 1 | 2;
65
+ verificationImpact?: string;
66
+ verificationConfidence?: {
67
+ level: 'high' | 'medium' | 'low' | 'none';
68
+ summary: string;
69
+ };
70
+ verification?: {
71
+ success: boolean;
72
+ lint?: {
73
+ success: boolean;
74
+ output?: string;
75
+ };
76
+ test?: {
77
+ success: boolean;
78
+ output?: string;
79
+ };
80
+ build?: {
81
+ success: boolean;
82
+ output?: string;
83
+ };
84
+ };
85
+ }
86
+ export type AnalysisReadyResult = {
87
+ status: 'ready';
88
+ } | {
89
+ status: 'pending';
90
+ } | {
91
+ status: 'error';
92
+ message: string;
93
+ };
94
+ /**
95
+ * Check if analysis results are available for a PR by polling the result endpoint.
96
+ * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
97
+ */
98
+ export declare function checkAnalysisReady(owner: string, repo: string, prNumber: number, token?: string): Promise<AnalysisReadyResult>;
99
+ /**
100
+ * Fetch analysis results once complete. Combines bug detection and rule violations.
101
+ */
102
+ export declare function fetchAnalysisResults(owner: string, repo: string, prNumber: number, token?: string): Promise<AnalysisVerdict>;
103
+ /**
104
+ * Fetch the full rating synthesis for a PR.
105
+ * This is the rich analysis data shown on the Haystack feed — includes
106
+ * verdict, rating, structured findings, verified bugs, and agent fix prompts.
107
+ */
108
+ export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Haystack Analysis API client.
3
+ *
4
+ * Polls the result endpoint by PR identifier to check if analysis is ready,
5
+ * then fetches detailed results (bug detection + rule violations).
6
+ *
7
+ * Analysis is triggered automatically by the GitHub App webhook when a PR
8
+ * is created or updated — the CLI does not need to trigger it directly.
9
+ */
10
+ // ============================================================================
11
+ // Constants
12
+ // ============================================================================
13
+ const PROD_API_BASE = 'https://8z9ssbamdj.execute-api.us-west-2.amazonaws.com/prod';
14
+ // ============================================================================
15
+ // API helpers
16
+ // ============================================================================
17
+ function apiHeaders(token) {
18
+ const headers = {
19
+ 'Cache-Control': 'no-cache',
20
+ 'User-Agent': 'Haystack-CLI',
21
+ };
22
+ if (token) {
23
+ headers['X-GitHub-Token'] = token;
24
+ }
25
+ return headers;
26
+ }
27
+ // ============================================================================
28
+ // API functions
29
+ // ============================================================================
30
+ /**
31
+ * Check if analysis results are available for a PR by polling the result endpoint.
32
+ * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
33
+ */
34
+ export async function checkAnalysisReady(owner, repo, prNumber, token) {
35
+ const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}`;
36
+ let response;
37
+ try {
38
+ response = await fetch(url, {
39
+ headers: apiHeaders(token),
40
+ });
41
+ }
42
+ catch (err) {
43
+ return { status: 'error', message: err.message };
44
+ }
45
+ if (response.status === 404)
46
+ return { status: 'pending' };
47
+ if (!response.ok) {
48
+ return { status: 'error', message: `HTTP ${response.status}` };
49
+ }
50
+ let data;
51
+ try {
52
+ data = await response.json();
53
+ }
54
+ catch {
55
+ return { status: 'error', message: 'Invalid JSON response' };
56
+ }
57
+ return data.status === 'COMPLETED' ? { status: 'ready' } : { status: 'pending' };
58
+ }
59
+ /**
60
+ * Fetch analysis results once complete. Combines bug detection and rule violations.
61
+ */
62
+ export async function fetchAnalysisResults(owner, repo, prNumber, token) {
63
+ const prIdentifier = `${owner}/${repo}#${prNumber}`;
64
+ const encodedId = encodeURIComponent(prIdentifier);
65
+ const reviewUrl = `https://haystackeditor.com/review/${owner}/${repo}/${prNumber}`;
66
+ const issues = [];
67
+ // Fetch bug detection
68
+ try {
69
+ const bugUrl = `${PROD_API_BASE}/v2/analysis/${encodedId}/bug_detection.json`;
70
+ const bugResp = await fetch(bugUrl, {
71
+ headers: apiHeaders(token),
72
+ });
73
+ if (bugResp.ok) {
74
+ const bugData = await bugResp.json();
75
+ if (bugData.bugs) {
76
+ for (const bug of bugData.bugs) {
77
+ issues.push({
78
+ file: bug.file || 'unknown',
79
+ line: bug.line,
80
+ message: bug.description || 'Bug detected',
81
+ severity: bug.severity === 'critical' || bug.severity === 'high' ? 'error' : 'warning',
82
+ source: 'bug-detection',
83
+ });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch {
89
+ // Bug detection not available — not fatal
90
+ }
91
+ // Fetch rule violations
92
+ try {
93
+ const rulesUrl = `${PROD_API_BASE}/v2/analysis/${encodedId}/rule_violations.json`;
94
+ const rulesResp = await fetch(rulesUrl, {
95
+ headers: apiHeaders(token),
96
+ });
97
+ if (rulesResp.ok) {
98
+ const rulesData = await rulesResp.json();
99
+ if (rulesData.violations) {
100
+ for (const v of rulesData.violations) {
101
+ issues.push({
102
+ file: v.file || 'unknown',
103
+ line: v.line,
104
+ message: v.message || `Rule violation: ${v.rule || 'unknown'}`,
105
+ severity: v.severity === 'error' ? 'error' : 'warning',
106
+ source: 'rule-violation',
107
+ });
108
+ }
109
+ }
110
+ }
111
+ }
112
+ catch {
113
+ // Rule violations not available — not fatal
114
+ }
115
+ // Fetch merge decision (to check if auto-merged)
116
+ let autoMerged = false;
117
+ try {
118
+ const mergeUrl = `${PROD_API_BASE}/v2/analysis/${encodedId}/merge_decision.json`;
119
+ const mergeResp = await fetch(mergeUrl, {
120
+ headers: apiHeaders(token),
121
+ });
122
+ if (mergeResp.ok) {
123
+ const mergeData = await mergeResp.json();
124
+ autoMerged = mergeData.autoMerged === true;
125
+ }
126
+ }
127
+ catch {
128
+ // Merge decision not available — not fatal
129
+ }
130
+ const bugCount = issues.filter(i => i.source === 'bug-detection').length;
131
+ const ruleViolationCount = issues.filter(i => i.source === 'rule-violation').length;
132
+ const warningCount = issues.filter(i => i.severity === 'warning').length;
133
+ const errorCount = issues.filter(i => i.severity === 'error').length;
134
+ const state = errorCount === 0 && warningCount === 0 ? 'good-to-merge' : 'needs-input';
135
+ // Build summary
136
+ let summary;
137
+ if (state === 'good-to-merge') {
138
+ summary = autoMerged ? 'No issues found. PR was auto-merged.' : 'No issues found. Safe to merge.';
139
+ }
140
+ else {
141
+ const parts = [];
142
+ if (errorCount > 0)
143
+ parts.push(`${errorCount} error(s)`);
144
+ if (warningCount > 0)
145
+ parts.push(`${warningCount} warning(s)`);
146
+ if (bugCount > 0)
147
+ parts.push(`${bugCount} bug(s)`);
148
+ if (ruleViolationCount > 0)
149
+ parts.push(`${ruleViolationCount} rule violation(s)`);
150
+ summary = parts.join(', ') + ' found.';
151
+ }
152
+ return {
153
+ state,
154
+ bugCount,
155
+ warningCount,
156
+ ruleViolationCount,
157
+ summary,
158
+ issues,
159
+ reviewUrl,
160
+ autoMerged,
161
+ };
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
+ */
168
+ export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
169
+ // Use v3/result endpoint (same as the Lambda serves generic files)
170
+ const url = `${PROD_API_BASE}/v3/result/${owner}/${repo}/${prNumber}/rating_synthesis.json`;
171
+ try {
172
+ const response = await fetch(url, {
173
+ headers: apiHeaders(token),
174
+ });
175
+ if (!response.ok)
176
+ return null;
177
+ const data = await response.json();
178
+ // Handle presigned URL redirect (Lambda returns { download_url } for large files)
179
+ // Presigned S3 URLs include auth in query params — do NOT forward the GitHub token
180
+ // (S3 rejects requests with both presigned auth and an Authorization/custom header)
181
+ if (data.download_url) {
182
+ const s3Resp = await fetch(data.download_url, {
183
+ headers: { 'User-Agent': 'Haystack-CLI' },
184
+ });
185
+ if (!s3Resp.ok)
186
+ return null;
187
+ return await s3Resp.json();
188
+ }
189
+ return data;
190
+ }
191
+ catch {
192
+ return null;
193
+ }
194
+ }
@@ -9,7 +9,7 @@ const CONFIG_FILENAME = '.haystack.json';
9
9
  */
10
10
  export async function findConfigPath(startDir = process.cwd()) {
11
11
  let dir = startDir;
12
- while (true) {
12
+ for (;;) {
13
13
  const configPath = path.join(dir, CONFIG_FILENAME);
14
14
  try {
15
15
  await fs.access(configPath);
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Git utilities for haystack submit command.
3
+ *
4
+ * Provides functions for git operations: branch management, push, remote parsing.
5
+ */
6
+ export { findGitRoot } from './hooks.js';
7
+ export interface GitRemoteInfo {
8
+ owner: string;
9
+ repo: string;
10
+ remoteName: string;
11
+ }
12
+ export interface GitBranchInfo {
13
+ currentBranch: string;
14
+ defaultBranch: string;
15
+ isClean: boolean;
16
+ hasUncommittedChanges: boolean;
17
+ hasUnpushedCommits: boolean;
18
+ }
19
+ /**
20
+ * Get the current branch name
21
+ */
22
+ export declare function getCurrentBranch(): string;
23
+ /**
24
+ * Get the default branch (main or master)
25
+ */
26
+ export declare function getDefaultBranch(): string;
27
+ /**
28
+ * Check if working directory has uncommitted changes
29
+ */
30
+ export declare function hasUncommittedChanges(): boolean;
31
+ /**
32
+ * Check if current branch has unpushed commits
33
+ */
34
+ export declare function hasUnpushedCommits(): boolean;
35
+ /**
36
+ * Get comprehensive branch info
37
+ */
38
+ export declare function getBranchInfo(): GitBranchInfo;
39
+ /**
40
+ * Create a new branch from current HEAD
41
+ */
42
+ export declare function createBranch(branchName: string): void;
43
+ /**
44
+ * Check if a branch exists locally
45
+ */
46
+ export declare function branchExists(branchName: string): boolean;
47
+ /**
48
+ * Check if a branch exists on remote
49
+ */
50
+ export declare function remoteBranchExists(remoteName: string, branchName: string): boolean;
51
+ /**
52
+ * Checkout an existing branch
53
+ */
54
+ export declare function checkoutBranch(branchName: string): void;
55
+ /**
56
+ * Push current branch to remote with upstream tracking
57
+ */
58
+ export declare function pushBranch(remoteName: string, branchName: string): void;
59
+ /**
60
+ * Parse remote URL to extract owner and repo
61
+ * Supports both SSH and HTTPS URLs
62
+ */
63
+ export declare function parseRemoteUrl(remoteName?: string): GitRemoteInfo;
64
+ /**
65
+ * Get the last commit message (first line only)
66
+ */
67
+ export declare function getLastCommitMessage(): string;
68
+ /**
69
+ * Get the last commit SHA (short)
70
+ */
71
+ export declare function getLastCommitSha(): string;
72
+ /**
73
+ * Get full commit messages (subject + body) for all commits since base branch.
74
+ * Returns the combined log, or empty string if no commits or on error.
75
+ */
76
+ export declare function getCommitMessagesSinceBase(baseBranch: string): string;
77
+ /**
78
+ * Get the number of commits ahead of base branch
79
+ */
80
+ export declare function getCommitsAhead(baseBranch: string): number;