@haystackeditor/cli 0.15.19 → 0.15.21

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 (66) hide show
  1. package/README.md +100 -1
  2. package/dist/assets/skills/map-your-system.md +9 -4
  3. package/dist/commands/ask.js +2 -2
  4. package/dist/commands/config.d.ts +9 -0
  5. package/dist/commands/config.js +46 -14
  6. package/dist/commands/dismiss.d.ts +15 -3
  7. package/dist/commands/dismiss.js +66 -116
  8. package/dist/commands/mcp.js +96 -1
  9. package/dist/commands/policy.js +31 -12
  10. package/dist/commands/pr-status.d.ts +125 -2
  11. package/dist/commands/pr-status.js +88 -73
  12. package/dist/commands/pr.d.ts +1 -7
  13. package/dist/commands/pr.js +10 -21
  14. package/dist/commands/request-review.d.ts +12 -1
  15. package/dist/commands/request-review.js +42 -65
  16. package/dist/commands/review.d.ts +1 -0
  17. package/dist/commands/review.js +58 -60
  18. package/dist/commands/setup.js +4 -1
  19. package/dist/commands/submit.d.ts +3 -0
  20. package/dist/commands/submit.js +178 -38
  21. package/dist/commands/system-map.d.ts +42 -0
  22. package/dist/commands/system-map.js +228 -0
  23. package/dist/commands/tokens.js +3 -2
  24. package/dist/commands/traces.js +2 -2
  25. package/dist/commands/triage.d.ts +1 -6
  26. package/dist/commands/triage.js +51 -55
  27. package/dist/commands/verify-core.d.ts +424 -0
  28. package/dist/commands/verify-core.js +753 -0
  29. package/dist/commands/verify-mcp.d.ts +8 -0
  30. package/dist/commands/verify-mcp.js +458 -0
  31. package/dist/commands/verify-ops.d.ts +158 -0
  32. package/dist/commands/verify-ops.js +1148 -0
  33. package/dist/commands/verify-reseal.d.ts +9 -0
  34. package/dist/commands/verify-reseal.js +148 -0
  35. package/dist/commands/verify-sandboxes.d.ts +95 -0
  36. package/dist/commands/verify-sandboxes.js +352 -0
  37. package/dist/commands/verify.d.ts +11 -0
  38. package/dist/commands/verify.js +97 -0
  39. package/dist/index.js +343 -20
  40. package/dist/schema.d.ts +4 -0
  41. package/dist/schema.js +4 -0
  42. package/dist/states.d.ts +29 -0
  43. package/dist/states.js +46 -0
  44. package/dist/triage/runner.d.ts +7 -0
  45. package/dist/triage/runner.js +2 -2
  46. package/dist/types.d.ts +54 -0
  47. package/dist/types.js +15 -0
  48. package/dist/utils/action-output.d.ts +24 -0
  49. package/dist/utils/action-output.js +26 -0
  50. package/dist/utils/analysis-api.d.ts +11 -0
  51. package/dist/utils/analysis-api.js +29 -3
  52. package/dist/utils/auth.js +7 -0
  53. package/dist/utils/git.d.ts +4 -1
  54. package/dist/utils/git.js +17 -7
  55. package/dist/utils/haystack-api.d.ts +15 -0
  56. package/dist/utils/haystack-api.js +58 -7
  57. package/dist/utils/pr-ref.d.ts +27 -0
  58. package/dist/utils/pr-ref.js +56 -0
  59. package/package.json +2 -2
  60. package/schemas/action.v1.json +22 -0
  61. package/schemas/error.v1.json +14 -0
  62. package/schemas/pr-status.v1.json +61 -0
  63. package/schemas/pr.v1.json +80 -17
  64. package/schemas/setup.v1.json +66 -12
  65. package/schemas/submit.v1.json +88 -0
  66. package/schemas/triage.v1.json +71 -18
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Canonical machine-readable PR-state vocabulary for every CLI JSON surface.
3
+ *
4
+ * One concept, one spelling: all `--json` output uses snake_case state tokens.
5
+ * Before this module existed the same state appeared as `good_to_merge`
6
+ * (triage/pr get/inbox) and `good-to-merge` (pr-status, raw server buckets),
7
+ * so no single parser worked across commands. Server responses that use
8
+ * kebab-case are normalized at the edge via `normalizeFeedBucket`.
9
+ */
10
+ /** Public triage verdict — emitted by `triage`, `pr get`, `submit`, `pr-status`. */
11
+ export declare const TRIAGE_VERDICTS: readonly ["good_to_merge", "needs_review", "needs_input"];
12
+ export type TriageVerdict = (typeof TRIAGE_VERDICTS)[number];
13
+ /**
14
+ * Map the server's internal analysis verdict ('clean' | 'has-issues' |
15
+ * 'needs-review', with a rating-based fallback for pre-verdict analyses) to
16
+ * the public triage verdict. This mapping used to be duplicated inline in
17
+ * triage.ts and pr.ts.
18
+ */
19
+ export declare function toPublicVerdict(analysisVerdict: string | null | undefined, haystackRating?: number | null): TriageVerdict;
20
+ /**
21
+ * Known feed buckets in canonical snake_case form. The server may introduce
22
+ * new buckets; `normalizeFeedBucket` converts mechanically so unknown buckets
23
+ * pass through in the same convention instead of crashing or leaking kebab-case.
24
+ */
25
+ export declare const FEED_BUCKETS: readonly ["analyzing", "auto_fixing", "good_to_merge", "failed_when_merging", "issues", "needs_assignment", "needs_shepherding", "needs_fixing", "awaiting_review", "review_requested", "snoozed"];
26
+ export type FeedBucket = (typeof FEED_BUCKETS)[number];
27
+ /** Normalize a server feed bucket (kebab-case) to the canonical snake_case form. */
28
+ export declare function normalizeFeedBucket(bucket: string): string;
29
+ export declare function normalizeFeedBucket(bucket: string | null | undefined): string | null;
package/dist/states.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Canonical machine-readable PR-state vocabulary for every CLI JSON surface.
3
+ *
4
+ * One concept, one spelling: all `--json` output uses snake_case state tokens.
5
+ * Before this module existed the same state appeared as `good_to_merge`
6
+ * (triage/pr get/inbox) and `good-to-merge` (pr-status, raw server buckets),
7
+ * so no single parser worked across commands. Server responses that use
8
+ * kebab-case are normalized at the edge via `normalizeFeedBucket`.
9
+ */
10
+ /** Public triage verdict — emitted by `triage`, `pr get`, `submit`, `pr-status`. */
11
+ export const TRIAGE_VERDICTS = ['good_to_merge', 'needs_review', 'needs_input'];
12
+ /**
13
+ * Map the server's internal analysis verdict ('clean' | 'has-issues' |
14
+ * 'needs-review', with a rating-based fallback for pre-verdict analyses) to
15
+ * the public triage verdict. This mapping used to be duplicated inline in
16
+ * triage.ts and pr.ts.
17
+ */
18
+ export function toPublicVerdict(analysisVerdict, haystackRating) {
19
+ const verdict = analysisVerdict ?? ((haystackRating ?? 0) >= 5 ? 'clean' : 'has-issues');
20
+ if (verdict === 'clean')
21
+ return 'good_to_merge';
22
+ if (verdict === 'needs-review')
23
+ return 'needs_review';
24
+ return 'needs_input';
25
+ }
26
+ /**
27
+ * Known feed buckets in canonical snake_case form. The server may introduce
28
+ * new buckets; `normalizeFeedBucket` converts mechanically so unknown buckets
29
+ * pass through in the same convention instead of crashing or leaking kebab-case.
30
+ */
31
+ export const FEED_BUCKETS = [
32
+ 'analyzing',
33
+ 'auto_fixing',
34
+ 'good_to_merge',
35
+ 'failed_when_merging',
36
+ 'issues',
37
+ 'needs_assignment',
38
+ 'needs_shepherding',
39
+ 'needs_fixing',
40
+ 'awaiting_review',
41
+ 'review_requested',
42
+ 'snoozed',
43
+ ];
44
+ export function normalizeFeedBucket(bucket) {
45
+ return bucket == null ? null : bucket.replace(/-/g, '_');
46
+ }
@@ -16,6 +16,13 @@ export interface RunTriageOptions {
16
16
  * Returns the first one found, or null if none available.
17
17
  */
18
18
  export declare function detectAgenticCLI(): AgenticCLI | null;
19
+ /**
20
+ * Build the CLI command + args for the detected agentic tool.
21
+ */
22
+ export declare function buildCommand(cli: AgenticCLI, prompt: string, maxTurns: number): {
23
+ command: string;
24
+ args: string[];
25
+ };
19
26
  /**
20
27
  * Run pre-PR triage checks using parallel sub-agent processes.
21
28
  *
@@ -86,7 +86,7 @@ export function detectAgenticCLI() {
86
86
  /**
87
87
  * Build the CLI command + args for the detected agentic tool.
88
88
  */
89
- function buildCommand(cli, prompt, maxTurns) {
89
+ export function buildCommand(cli, prompt, maxTurns) {
90
90
  switch (cli) {
91
91
  case 'claude':
92
92
  return {
@@ -101,7 +101,7 @@ function buildCommand(cli, prompt, maxTurns) {
101
101
  case 'codex':
102
102
  return {
103
103
  command: 'codex',
104
- args: ['exec', prompt, '--full-auto', '--ephemeral'],
104
+ args: ['exec', prompt, '--full-auto'],
105
105
  };
106
106
  case 'gemini':
107
107
  return {
package/dist/types.d.ts CHANGED
@@ -719,11 +719,52 @@ declare const TriageConfigSchema: z.ZodObject<{
719
719
  maxTurns?: Partial<Record<"code-review" | "rules-validator" | "intent-drift", number>> | undefined;
720
720
  timeoutMs?: number | undefined;
721
721
  }>;
722
+ declare const PreferencesSchema: z.ZodObject<{
723
+ /** Run analysis automatically for PR webhook events (default: true) */
724
+ automatic_analysis: z.ZodOptional<z.ZodBoolean>;
725
+ /** Enable sandbox-backed verification for this repository */
726
+ sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
727
+ /** Enroll submitted PRs in auto-merge by default */
728
+ auto_merge: z.ZodOptional<z.ZodBoolean>;
729
+ /** Allow Haystack to attempt automatic fixes */
730
+ auto_fix: z.ZodOptional<z.ZodBoolean>;
731
+ }, "strip", z.ZodTypeAny, {
732
+ automatic_analysis?: boolean | undefined;
733
+ sandbox_enabled?: boolean | undefined;
734
+ auto_merge?: boolean | undefined;
735
+ auto_fix?: boolean | undefined;
736
+ }, {
737
+ automatic_analysis?: boolean | undefined;
738
+ sandbox_enabled?: boolean | undefined;
739
+ auto_merge?: boolean | undefined;
740
+ auto_fix?: boolean | undefined;
741
+ }>;
722
742
  export declare const HaystackConfigSchema: z.ZodObject<{
723
743
  /** Config version (must be "1") */
724
744
  version: z.ZodLiteral<"1">;
725
745
  /** Project name (for display) */
726
746
  name: z.ZodOptional<z.ZodString>;
747
+ /** Repository-level Haystack behavior */
748
+ preferences: z.ZodOptional<z.ZodObject<{
749
+ /** Run analysis automatically for PR webhook events (default: true) */
750
+ automatic_analysis: z.ZodOptional<z.ZodBoolean>;
751
+ /** Enable sandbox-backed verification for this repository */
752
+ sandbox_enabled: z.ZodOptional<z.ZodBoolean>;
753
+ /** Enroll submitted PRs in auto-merge by default */
754
+ auto_merge: z.ZodOptional<z.ZodBoolean>;
755
+ /** Allow Haystack to attempt automatic fixes */
756
+ auto_fix: z.ZodOptional<z.ZodBoolean>;
757
+ }, "strip", z.ZodTypeAny, {
758
+ automatic_analysis?: boolean | undefined;
759
+ sandbox_enabled?: boolean | undefined;
760
+ auto_merge?: boolean | undefined;
761
+ auto_fix?: boolean | undefined;
762
+ }, {
763
+ automatic_analysis?: boolean | undefined;
764
+ sandbox_enabled?: boolean | undefined;
765
+ auto_merge?: boolean | undefined;
766
+ auto_fix?: boolean | undefined;
767
+ }>>;
727
768
  /** Package manager override (auto-detected if not specified) */
728
769
  package_manager: z.ZodOptional<z.ZodEnum<["npm", "pnpm", "yarn", "bun"]>>;
729
770
  /** Development server configuration */
@@ -1315,6 +1356,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
1315
1356
  root?: string | undefined;
1316
1357
  depends_on?: string[] | undefined;
1317
1358
  }> | undefined;
1359
+ preferences?: {
1360
+ automatic_analysis?: boolean | undefined;
1361
+ sandbox_enabled?: boolean | undefined;
1362
+ auto_merge?: boolean | undefined;
1363
+ auto_fix?: boolean | undefined;
1364
+ } | undefined;
1318
1365
  package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
1319
1366
  dev_server?: {
1320
1367
  command: string;
@@ -1442,6 +1489,12 @@ export declare const HaystackConfigSchema: z.ZodObject<{
1442
1489
  root?: string | undefined;
1443
1490
  depends_on?: string[] | undefined;
1444
1491
  }> | undefined;
1492
+ preferences?: {
1493
+ automatic_analysis?: boolean | undefined;
1494
+ sandbox_enabled?: boolean | undefined;
1495
+ auto_merge?: boolean | undefined;
1496
+ auto_fix?: boolean | undefined;
1497
+ } | undefined;
1445
1498
  package_manager?: "npm" | "pnpm" | "yarn" | "bun" | undefined;
1446
1499
  dev_server?: {
1447
1500
  command: string;
@@ -1559,6 +1612,7 @@ export type NetworkConfig = z.infer<typeof NetworkConfigSchema>;
1559
1612
  export type SecretDeclaration = z.infer<typeof SecretDeclarationSchema>;
1560
1613
  export type MergeQueueConfig = z.infer<typeof MergeQueueConfigSchema>;
1561
1614
  export type TriageConfig = z.infer<typeof TriageConfigSchema>;
1615
+ export type Preferences = z.infer<typeof PreferencesSchema>;
1562
1616
  /**
1563
1617
  * Project detection results
1564
1618
  */
package/dist/types.js CHANGED
@@ -280,6 +280,19 @@ const TriageConfigSchema = z.object({
280
280
  timeoutMs: z.number().int().positive().optional(),
281
281
  });
282
282
  // =============================================================================
283
+ // REPOSITORY PREFERENCES
284
+ // =============================================================================
285
+ const PreferencesSchema = z.object({
286
+ /** Run analysis automatically for PR webhook events (default: true) */
287
+ automatic_analysis: z.boolean().optional(),
288
+ /** Enable sandbox-backed verification for this repository */
289
+ sandbox_enabled: z.boolean().optional(),
290
+ /** Enroll submitted PRs in auto-merge by default */
291
+ auto_merge: z.boolean().optional(),
292
+ /** Allow Haystack to attempt automatic fixes */
293
+ auto_fix: z.boolean().optional(),
294
+ });
295
+ // =============================================================================
283
296
  // MAIN CONFIG
284
297
  // =============================================================================
285
298
  export const HaystackConfigSchema = z.object({
@@ -287,6 +300,8 @@ export const HaystackConfigSchema = z.object({
287
300
  version: z.literal('1'),
288
301
  /** Project name (for display) */
289
302
  name: z.string().optional(),
303
+ /** Repository-level Haystack behavior */
304
+ preferences: PreferencesSchema.optional(),
290
305
  /** Package manager override (auto-detected if not specified) */
291
306
  package_manager: z.enum(['npm', 'pnpm', 'yarn', 'bun']).optional(),
292
307
  /** Development server configuration */
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared machine-readable envelope for the mutating commands
3
+ * (dismiss, mark-reviewed, undismiss, request-review, review).
4
+ *
5
+ * One schema for all of them (`haystack schema action`): every payload
6
+ * carries `action`, `status`, and `ref`, so an agent can drive any state
7
+ * change with a single parser. Success payloads go to stdout as the only
8
+ * stdout content; human/progress output stays on stderr in --json mode.
9
+ */
10
+ export type ActionName = 'dismiss' | 'mark_reviewed' | 'undismiss' | 'request_review' | 'trigger_review' | 'clear_pending';
11
+ export interface ActionPayload extends Record<string, unknown> {
12
+ action: ActionName;
13
+ status: 'ok';
14
+ ref: string;
15
+ /** Human-readable statement of what changed. */
16
+ detail: string;
17
+ }
18
+ /** Print the success envelope (JSON mode only — human output is the caller's). */
19
+ export declare function emitActionPayload(payload: ActionPayload): void;
20
+ /**
21
+ * Print a failure (JSON error envelope on stdout when --json, red text on
22
+ * stderr always) and exit 1.
23
+ */
24
+ export declare function failAction(json: boolean | undefined, action: ActionName, ref: string | null, message: string): never;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared machine-readable envelope for the mutating commands
3
+ * (dismiss, mark-reviewed, undismiss, request-review, review).
4
+ *
5
+ * One schema for all of them (`haystack schema action`): every payload
6
+ * carries `action`, `status`, and `ref`, so an agent can drive any state
7
+ * change with a single parser. Success payloads go to stdout as the only
8
+ * stdout content; human/progress output stays on stderr in --json mode.
9
+ */
10
+ import chalk from 'chalk';
11
+ import { withSchema } from '../schema.js';
12
+ /** Print the success envelope (JSON mode only — human output is the caller's). */
13
+ export function emitActionPayload(payload) {
14
+ console.log(JSON.stringify(withSchema('action', payload), null, 2));
15
+ }
16
+ /**
17
+ * Print a failure (JSON error envelope on stdout when --json, red text on
18
+ * stderr always) and exit 1.
19
+ */
20
+ export function failAction(json, action, ref, message) {
21
+ if (json) {
22
+ process.stdout.write(JSON.stringify(withSchema('action', { action, status: 'error', ref, error: message }), null, 2) + '\n');
23
+ }
24
+ console.error(chalk.red(`\n${message}\n`));
25
+ process.exit(1);
26
+ }
@@ -172,5 +172,16 @@ export interface AutoFixManifest {
172
172
  timestamp: string;
173
173
  resultSha?: string;
174
174
  }
175
+ /**
176
+ * Null means "artifact does not exist (yet)" — 404s and transient
177
+ * network/parse failures. Auth failures (401/403) THROW a classified
178
+ * HaystackApiError instead: collapsing them into null used to make an
179
+ * expired token read as "not available yet — try again in a moment",
180
+ * sending agents into a retry loop when the fix was `haystack login`.
181
+ */
175
182
  export declare function fetchAutoFixManifest(owner: string, repo: string, prNumber: number, token?: string): Promise<AutoFixManifest | null>;
183
+ /**
184
+ * Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
185
+ * a classified HaystackApiError — see fetchAutoFixManifest for why.
186
+ */
176
187
  export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
@@ -7,6 +7,7 @@
7
7
  * Analysis is triggered automatically by the GitHub App webhook when a PR
8
8
  * is created or updated — the CLI does not need to trigger it directly.
9
9
  */
10
+ import { classifyHttpError } from './haystack-api.js';
10
11
  // ============================================================================
11
12
  // Constants
12
13
  // ============================================================================
@@ -187,7 +188,9 @@ export async function checkAnalysisReady(owner, repo, prNumber, token) {
187
188
  if (response.status === 404)
188
189
  return { status: 'pending' };
189
190
  if (!response.ok) {
190
- return { status: 'error', message: `HTTP ${response.status}` };
191
+ // Classified, actionable message — a bare "HTTP 403" used to send agents
192
+ // into a retry loop when the fix was `haystack login`.
193
+ return { status: 'error', message: (await classifyHttpError(response, 'Analysis status check')).message };
191
194
  }
192
195
  let data;
193
196
  try {
@@ -336,12 +339,22 @@ export async function fetchAnalysisResults(owner, repo, prNumber, token) {
336
339
  needsHumanReview,
337
340
  };
338
341
  }
342
+ /**
343
+ * Null means "artifact does not exist (yet)" — 404s and transient
344
+ * network/parse failures. Auth failures (401/403) THROW a classified
345
+ * HaystackApiError instead: collapsing them into null used to make an
346
+ * expired token read as "not available yet — try again in a moment",
347
+ * sending agents into a retry loop when the fix was `haystack login`.
348
+ */
339
349
  export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
340
350
  const url = v3ResultUrl(owner, repo, prNumber, 'auto-fix-manifest.json');
341
351
  try {
342
352
  const response = await fetch(url, {
343
353
  headers: apiHeaders(token),
344
354
  });
355
+ if (response.status === 401 || response.status === 403) {
356
+ throw await classifyHttpError(response, 'Auto-fix manifest fetch');
357
+ }
345
358
  if (!response.ok)
346
359
  return null;
347
360
  const data = await response.json();
@@ -355,10 +368,17 @@ export async function fetchAutoFixManifest(owner, repo, prNumber, token) {
355
368
  }
356
369
  return data;
357
370
  }
358
- catch {
371
+ catch (err) {
372
+ if (err instanceof Error && err.name === 'HaystackApiError')
373
+ throw err;
374
+ // Transient network/parse failure — same "not available" semantics as 404.
359
375
  return null;
360
376
  }
361
377
  }
378
+ /**
379
+ * Null means "synthesis does not exist (yet)". Auth failures (401/403) THROW
380
+ * a classified HaystackApiError — see fetchAutoFixManifest for why.
381
+ */
362
382
  export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
363
383
  // Route through the auth-worker (/api/analysis/{prId}/rating_synthesis.json)
364
384
  // so hsk_live tokens are introspected and the scope gate applies — see
@@ -368,6 +388,9 @@ export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
368
388
  const response = await fetch(url, {
369
389
  headers: apiHeaders(token),
370
390
  });
391
+ if (response.status === 401 || response.status === 403) {
392
+ throw await classifyHttpError(response, 'Rating synthesis fetch');
393
+ }
371
394
  if (!response.ok)
372
395
  return null;
373
396
  const data = await response.json();
@@ -384,7 +407,10 @@ export async function fetchRatingSynthesis(owner, repo, prNumber, token) {
384
407
  }
385
408
  return data;
386
409
  }
387
- catch {
410
+ catch (err) {
411
+ if (err instanceof Error && err.name === 'HaystackApiError')
412
+ throw err;
413
+ // Transient network/parse failure — same "not available" semantics as 404.
388
414
  return null;
389
415
  }
390
416
  }
@@ -399,6 +399,13 @@ export async function resolveAuthContext(options = {}) {
399
399
  // — that intent should not be overridden by a stale OAuth cookie.
400
400
  const headless = await readHeadlessToken();
401
401
  if (headless) {
402
+ // The headless token wins, but dropping an explicit --account silently
403
+ // would leave the caller authenticated as someone they didn't ask for —
404
+ // say so on stderr.
405
+ if (options.preferredLogin) {
406
+ console.error(`--account ${options.preferredLogin} is ignored: a headless token (${headless.label ?? 'token file'}) takes precedence. ` +
407
+ 'Unset HAYSTACK_CLI_TOKEN or remove the token file (`haystack logout --headless`) to use saved accounts.');
408
+ }
402
409
  return {
403
410
  login: headless.label ?? 'headless',
404
411
  token: headless.token,
@@ -78,7 +78,10 @@ export declare class UnsupportedRemoteUrlError extends Error {
78
78
  */
79
79
  export declare function parseRemoteUrl(remoteName?: string): GitRemoteInfo;
80
80
  /**
81
- * Get the last commit message (first line only)
81
+ * Get the last commit message (first line only).
82
+ *
83
+ * Throws instead of guessing: this feeds the default PR title, and the old
84
+ * silent fallback ('Update') shipped meaningless titles when git failed.
82
85
  */
83
86
  export declare function getLastCommitMessage(): string;
84
87
  /**
package/dist/utils/git.js CHANGED
@@ -22,8 +22,10 @@ export function getCurrentBranch() {
22
22
  stdio: ['pipe', 'pipe', 'pipe'],
23
23
  }).trim();
24
24
  }
25
- catch {
26
- throw new Error('Failed to get current branch. Are you in a git repository?');
25
+ catch (err) {
26
+ const stderr = err?.stderr?.toString().trim();
27
+ throw new Error(`Failed to get the current branch${stderr ? ` (${stderr})` : ''}. ` +
28
+ 'Run from inside a git repository.');
27
29
  }
28
30
  }
29
31
  /**
@@ -296,19 +298,25 @@ export function parseRemoteUrl(remoteName = 'origin') {
296
298
  }
297
299
  catch (err) {
298
300
  // Propagate the "unrecognized URL" case as-is; wrap everything else (the
299
- // `git remote get-url` command itself failing) in a friendlier message.
301
+ // `git remote get-url` command itself failing) with the underlying git
302
+ // error preserved — "No such remote 'origin'" is the actual diagnosis.
300
303
  // Classify by type, not message text (see pr-rules.yml PR007).
301
304
  if (err instanceof UnsupportedRemoteUrlError) {
302
305
  throw err;
303
306
  }
304
- throw new Error(`Failed to get remote URL for '${remoteName}'`);
307
+ const stderr = err?.stderr?.toString().trim();
308
+ const cause = stderr || (err instanceof Error ? err.message : String(err));
309
+ throw new Error(`Failed to get remote URL for '${remoteName}': ${cause}`);
305
310
  }
306
311
  }
307
312
  // ============================================================================
308
313
  // Commit operations
309
314
  // ============================================================================
310
315
  /**
311
- * Get the last commit message (first line only)
316
+ * Get the last commit message (first line only).
317
+ *
318
+ * Throws instead of guessing: this feeds the default PR title, and the old
319
+ * silent fallback ('Update') shipped meaningless titles when git failed.
312
320
  */
313
321
  export function getLastCommitMessage() {
314
322
  try {
@@ -317,8 +325,10 @@ export function getLastCommitMessage() {
317
325
  stdio: ['pipe', 'pipe', 'pipe'],
318
326
  }).trim();
319
327
  }
320
- catch {
321
- return 'Update';
328
+ catch (err) {
329
+ const stderr = err?.stderr?.toString().trim();
330
+ throw new Error(`Could not read the last commit message${stderr ? ` (${stderr})` : ''}. ` +
331
+ 'Commit your changes first, or pass an explicit --title.');
322
332
  }
323
333
  }
324
334
  /**
@@ -12,6 +12,21 @@ export interface CliIdentity {
12
12
  login: string;
13
13
  id: number;
14
14
  }
15
+ /**
16
+ * Typed error for non-OK Haystack API responses. Callers classify by
17
+ * `status` (never by message substring) and the message always carries a
18
+ * remediation an agent can act on.
19
+ */
20
+ export declare class HaystackApiError extends Error {
21
+ readonly status: number;
22
+ constructor(status: number, message: string);
23
+ get isAuthError(): boolean;
24
+ }
25
+ /**
26
+ * Build an actionable error for a non-OK response: what failed, the HTTP
27
+ * class it failed with, and the next command to run.
28
+ */
29
+ export declare function classifyHttpError(response: Response, context: string): Promise<HaystackApiError>;
15
30
  export declare function haystackJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
16
31
  export declare function fetchCliIdentity(token: string): Promise<CliIdentity>;
17
32
  export declare function fetchHaystackInstallations(token: string): Promise<UserInstallation[]>;
@@ -1,4 +1,20 @@
1
1
  const HAYSTACK_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
2
+ /**
3
+ * Typed error for non-OK Haystack API responses. Callers classify by
4
+ * `status` (never by message substring) and the message always carries a
5
+ * remediation an agent can act on.
6
+ */
7
+ export class HaystackApiError extends Error {
8
+ status;
9
+ constructor(status, message) {
10
+ super(message);
11
+ this.name = 'HaystackApiError';
12
+ this.status = status;
13
+ }
14
+ get isAuthError() {
15
+ return this.status === 401 || this.status === 403;
16
+ }
17
+ }
2
18
  function headers(token, extra) {
3
19
  const result = new Headers(extra);
4
20
  result.set('Authorization', `Bearer ${token}`);
@@ -6,17 +22,52 @@ function headers(token, extra) {
6
22
  result.set('User-Agent', 'Haystack-CLI');
7
23
  return result;
8
24
  }
9
- async function errorMessage(response) {
10
- const text = await response.text().catch(() => '');
25
+ /** Extract the server's own message if the body is JSON; truncate raw bodies. */
26
+ async function serverMessage(response) {
27
+ let text;
28
+ try {
29
+ text = await response.text();
30
+ }
31
+ catch (err) {
32
+ // A transport failure while reading the error body is itself signal —
33
+ // say so instead of reporting it as "no detail" (the status-based
34
+ // classification in classifyHttpError stands either way).
35
+ return `error body unreadable: ${err instanceof Error ? err.message : String(err)}`;
36
+ }
11
37
  if (!text)
12
- return `HTTP ${response.status}`;
38
+ return null;
13
39
  try {
14
40
  const parsed = JSON.parse(text);
15
- return parsed.message ?? parsed.error ?? text;
41
+ return parsed.message ?? parsed.error ?? null;
16
42
  }
17
43
  catch {
18
- return text;
44
+ // Not JSON (HTML error page, plain text) — never dump a full raw body.
45
+ return text.slice(0, 200);
46
+ }
47
+ }
48
+ /**
49
+ * Build an actionable error for a non-OK response: what failed, the HTTP
50
+ * class it failed with, and the next command to run.
51
+ */
52
+ export async function classifyHttpError(response, context) {
53
+ const detail = await serverMessage(response);
54
+ const suffix = detail ? ` (${detail})` : '';
55
+ if (response.status === 401) {
56
+ return new HaystackApiError(401, `${context}: authentication failed${suffix}. Run \`haystack login\` to re-authenticate.`);
57
+ }
58
+ if (response.status === 403) {
59
+ return new HaystackApiError(403, `${context}: access denied${suffix}. Your token may lack access to this repository — run \`haystack auth list\` to check accounts, or \`haystack login\` to add one.`);
60
+ }
61
+ if (response.status === 404) {
62
+ return new HaystackApiError(404, `${context}: not found${suffix}. Check the PR number and repository.`);
63
+ }
64
+ if (response.status === 429) {
65
+ return new HaystackApiError(429, `${context}: rate limited${suffix}. Wait before retrying.`);
66
+ }
67
+ if (response.status >= 500) {
68
+ return new HaystackApiError(response.status, `${context}: Haystack server error (HTTP ${response.status})${suffix}. Retry in a moment.`);
19
69
  }
70
+ return new HaystackApiError(response.status, `${context}: unexpected response (HTTP ${response.status})${suffix}.`);
20
71
  }
21
72
  export async function haystackJson(path, token, init = {}) {
22
73
  const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
@@ -24,7 +75,7 @@ export async function haystackJson(path, token, init = {}) {
24
75
  headers: headers(token, init.headers),
25
76
  });
26
77
  if (!response.ok) {
27
- throw new Error(`${response.status} ${await errorMessage(response)}`);
78
+ throw await classifyHttpError(response, `Haystack API ${path}`);
28
79
  }
29
80
  return response.json();
30
81
  }
@@ -46,7 +97,7 @@ export async function fetchAnalysisArtifact(owner, repo, prNumber, file, token)
46
97
  if (response.status === 404)
47
98
  return null;
48
99
  if (!response.ok) {
49
- throw new Error(`${response.status} ${await errorMessage(response)}`);
100
+ throw await classifyHttpError(response, `Analysis artifact ${file}`);
50
101
  }
51
102
  const data = await response.json();
52
103
  if (!data || typeof data !== 'object' || !('download_url' in data) || !data.download_url) {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared PR-identifier parsing for every command that takes a PR ref.
3
+ *
4
+ * One grammar, one error message, one repo-inference path. This used to be
5
+ * duplicated per command, and the copies drifted: different "accepted formats"
6
+ * lists (none mentioned the `#123` form the regex accepted), and one copy let
7
+ * a raw git error escape where the others gave a friendly hint. Commands must
8
+ * import from here rather than reimplementing the grammar.
9
+ *
10
+ * Accepted formats, in the order they are tried:
11
+ * https://github.com/owner/repo/pull/123 GitHub URL
12
+ * owner/repo#123 Fully qualified
13
+ * 123 or #123 PR number (repo inferred from the `origin` remote)
14
+ */
15
+ export interface ParsedPR {
16
+ owner: string;
17
+ repo: string;
18
+ prNumber: number;
19
+ }
20
+ /** The accepted-formats block used in parse errors and help text. */
21
+ export declare function acceptedPrRefFormats(commandName: string): string;
22
+ /**
23
+ * Parse a PR identifier. `commandName` is interpolated into error messages so
24
+ * the agent can copy a corrected invocation verbatim (e.g. "triage",
25
+ * "pr get", "traces list").
26
+ */
27
+ export declare function parsePrRef(identifier: string, commandName: string): ParsedPR;