@haystackeditor/cli 0.15.17 → 0.15.19

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 (46) hide show
  1. package/README.md +45 -6
  2. package/dist/assets/skills/map-your-system.md +138 -0
  3. package/dist/commands/ask.d.ts +14 -0
  4. package/dist/commands/ask.js +20 -0
  5. package/dist/commands/inbox.d.ts +65 -0
  6. package/dist/commands/inbox.js +137 -0
  7. package/dist/commands/mcp.js +88 -134
  8. package/dist/commands/pr.d.ts +37 -1
  9. package/dist/commands/pr.js +95 -77
  10. package/dist/commands/review.d.ts +24 -0
  11. package/dist/commands/review.js +193 -0
  12. package/dist/commands/schema-cmd.js +1 -1
  13. package/dist/commands/skills.js +4 -0
  14. package/dist/commands/traces.d.ts +48 -0
  15. package/dist/commands/traces.js +92 -0
  16. package/dist/commands/triage.d.ts +12 -0
  17. package/dist/commands/triage.js +90 -58
  18. package/dist/commands/webhooks.js +3 -6
  19. package/dist/index.js +79 -148
  20. package/dist/schema.d.ts +5 -2
  21. package/dist/schema.js +5 -2
  22. package/dist/utils/analysis-api.d.ts +40 -0
  23. package/dist/utils/analysis-api.js +108 -3
  24. package/dist/utils/haystack-api.d.ts +20 -0
  25. package/dist/utils/haystack-api.js +66 -0
  26. package/package.json +4 -4
  27. package/schemas/ask.v1.json +40 -0
  28. package/schemas/inbox.v1.json +27 -0
  29. package/schemas/pr.v2.json +54 -0
  30. package/schemas/traces.v1.json +53 -0
  31. package/schemas/triage.v2.json +64 -0
  32. package/dist/assets/skills/install-verification.md +0 -67
  33. package/dist/commands/verification.d.ts +0 -21
  34. package/dist/commands/verification.js +0 -291
  35. package/dist/verification/contract.d.ts +0 -240
  36. package/dist/verification/contract.js +0 -115
  37. package/dist/verification/init.d.ts +0 -10
  38. package/dist/verification/init.js +0 -243
  39. package/dist/verification/manifest.d.ts +0 -49
  40. package/dist/verification/manifest.js +0 -186
  41. package/dist/verification/runner.d.ts +0 -20
  42. package/dist/verification/runner.js +0 -102
  43. package/dist/verification/safety.d.ts +0 -10
  44. package/dist/verification/safety.js +0 -252
  45. package/dist/verification/validate.d.ts +0 -12
  46. package/dist/verification/validate.js +0 -219
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { readFileSync } from 'node:fs';
23
23
  import { fileURLToPath } from 'node:url';
24
24
  import { dirname, join } from 'node:path';
25
25
  import chalk from 'chalk';
26
- import { Command } from 'commander';
26
+ import { Command, Option } from 'commander';
27
27
  import { statusCommand } from './commands/status.js';
28
28
  import { initCommand } from './commands/init.js';
29
29
  import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
@@ -36,12 +36,16 @@ import { listPolicies, addPolicy, removePolicy, initPolicies, addInstruction } f
36
36
  import { triageCommand } from './commands/triage.js';
37
37
  import { dismissCommand, markReviewedCommand, undismissCommand } from './commands/dismiss.js';
38
38
  import { requestReviewCommand } from './commands/request-review.js';
39
+ import { reviewCommand } from './commands/review.js';
39
40
  import { prStatusCommand } from './commands/pr-status.js';
41
+ import { prReadCommand } from './commands/pr.js';
42
+ import { inboxListCommand } from './commands/inbox.js';
43
+ import { askHaystackCommand } from './commands/ask.js';
44
+ import { tracesGetCommand, tracesListCommand } from './commands/traces.js';
40
45
  import { setupCommand } from './commands/setup.js';
41
46
  import { schemaCommand, listSchemas } from './commands/schema-cmd.js';
42
47
  import { registerWebhook, listWebhooks, rotateWebhookSecret, setWebhookEnabled, listDeliveries, replayDelivery, } from './commands/webhooks.js';
43
48
  import { editRulesCommand, validateRulesCommand } from './commands/rules.js';
44
- import { verificationInitCommand, verificationValidateCommand, verificationPreflightCommand, verificationRunCommand, verificationCollectCommand, verificationCheckSafetyCommand, } from './commands/verification.js';
45
49
  import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeTokenCommand, } from './commands/tokens.js';
46
50
  // `mcp` is imported lazily inside its command action (below), NOT at the top level.
47
51
  // It is the only module that pulls in `@modelcontextprotocol/sdk`; keeping it out of
@@ -49,11 +53,15 @@ import { headlessLoginCommand, headlessLogoutCommand, listTokensCommand, revokeT
49
53
  // like `triage`/`submit`. (0.15.12 shipped `mcp.js` but published its package.json
50
54
  // WITHOUT `@modelcontextprotocol/sdk`, so the eager import here hard-crashed every
51
55
  // command at startup with ERR_MODULE_NOT_FOUND.)
52
- // pr-state read helpers (parsePrRef, prReadCommand) are imported by the MCP
53
- // server directly from ./commands/pr.js; no top-level CLI command is wired
54
- // here. The previous `pr <ref> [show|merge|snooze|dismiss]` and `fix <ref>`
55
- // surfaces were scope creep on the CLI-as-platform spec; `haystack pr-status`
56
- // already covered the read use case.
56
+ async function runPublicCommand(action) {
57
+ try {
58
+ await action();
59
+ }
60
+ catch (err) {
61
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
62
+ process.exitCode = 1;
63
+ }
64
+ }
57
65
  /** Read the published version from package.json (dist/index.js → ../package.json)
58
66
  * so `haystack --version` can't drift from the package version. Falls back
59
67
  * gracefully so the flag never throws if the file can't be read. */
@@ -77,7 +85,7 @@ program
77
85
  .version(getVersion());
78
86
  program
79
87
  .command('schema [name]')
80
- .description('Print the JSON Schema for a command\'s --json output (triage, setup, inbox, pr, chat)')
88
+ .description('Print the JSON Schema for a command\'s --json output')
81
89
  .action((name) => {
82
90
  if (!name) {
83
91
  listSchemas();
@@ -237,7 +245,7 @@ program
237
245
  .option('--draft', 'Create as draft PR')
238
246
  .option('--review [reviewer]', 'Request human review (BLOCKS auto-merge — only use when explicitly asked)')
239
247
  .option('--force', 'Skip pre-PR triage checks')
240
- .option('--auto-fix', 'Alpha auto-fix for straightforward mechanical issues (default: from .haystack.json preferences.auto_fix; --no-auto-fix to disable)')
248
+ .addOption(new Option('--auto-fix', 'Alpha auto-fix for entitled repositories').hideHelp())
241
249
  .option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
242
250
  .option('--no-wait', 'Skip waiting for analysis results')
243
251
  .option('--max-turns <n>', 'Max agentic turns per triage checker (overrides .haystack.json triage.maxTurns)', (v) => parseInt(v, 10))
@@ -258,8 +266,6 @@ Pre-PR Triage:
258
266
 
259
267
  Use --force to skip triage entirely.
260
268
  Use --no-wait to skip waiting for analysis results.
261
- Use --auto-fix only when explicitly opting into the alpha fixer for
262
- straightforward mechanical issues.
263
269
 
264
270
  Triage budgets:
265
271
  • --max-turns <n> Raise/lower the per-checker tool-use turn cap.
@@ -282,10 +288,6 @@ Review Routing:
282
288
  enters the auto-merge queue after submission. Otherwise it still runs the
283
289
  same Haystack analysis workflow without enabling auto-merge.
284
290
 
285
- ⚠ --auto-fix is currently alpha and discouraged by default. Prefer plain
286
- haystack submit unless you explicitly want the sandbox fixer to attempt
287
- straightforward mechanical fixes before issues hit the Feed.
288
-
289
291
  ⚠ Only use --review when the user EXPLICITLY asks for human review.
290
292
  --review BLOCKS auto-merge — the PR will NOT merge until a human approves,
291
293
  which delays merging. Do not add --review "just to be safe".
@@ -296,7 +298,6 @@ Review Routing:
296
298
 
297
299
  Examples:
298
300
  haystack submit # Recommended: triage, create PR, auto-merge queue
299
- haystack submit --auto-fix # Discouraged alpha: auto-fix straightforward mechanical issues
300
301
  haystack submit --force # Skip triage
301
302
  haystack submit --no-wait # Don't wait for analysis
302
303
  haystack submit --title "Fix auth" # Custom PR title
@@ -358,6 +359,37 @@ Examples:
358
359
  haystack triage --clear # Clear pending state
359
360
  `)
360
361
  .action(triageCommand);
362
+ const inbox = program.command('inbox').description('List Haystack work needing your attention');
363
+ inbox
364
+ .command('list')
365
+ .description('List PRs in your Haystack inbox')
366
+ .option('--json', 'Output as JSON')
367
+ .action((options) => runPublicCommand(() => inboxListCommand(options)));
368
+ const prProgram = program.command('pr').description('Inspect one pull request');
369
+ prProgram
370
+ .command('get <ref>')
371
+ .description('Get triage, merge blockers, and trace availability')
372
+ .option('--json', 'Output as JSON')
373
+ .action((ref, options) => runPublicCommand(() => prReadCommand(ref, options)));
374
+ program
375
+ .command('ask <ref> <question>')
376
+ .description('Ask Haystack about a pull request')
377
+ .option('--json', 'Output the answer and every consulted customer-facing source as JSON')
378
+ .option('--session <id>', 'Continue a previous Ask Haystack session')
379
+ .action((ref, question, options) => runPublicCommand(() => askHaystackCommand(ref, question, options)));
380
+ const traces = program.command('traces').description('Inspect customer-owned Entire traces');
381
+ traces
382
+ .command('list <ref>')
383
+ .description('List retained checkpoints for a pull request')
384
+ .option('--json', 'Output as JSON')
385
+ .action((ref, options) => runPublicCommand(() => tracesListCommand(ref, options)));
386
+ traces
387
+ .command('get <ref> <checkpoint>')
388
+ .description('Get retained transcript chunks for one checkpoint')
389
+ .option('--cursor <cursor>', 'Zero-based chunk cursor')
390
+ .option('--limit <count>', 'Chunks per page', '20')
391
+ .option('--json', 'Output as JSON')
392
+ .action((ref, checkpoint, options) => runPublicCommand(() => tracesGetCommand(ref, checkpoint, options)));
361
393
  program
362
394
  .command('dismiss <pr>')
363
395
  .description('Dismiss analysis findings for a PR')
@@ -418,6 +450,34 @@ Examples:
418
450
  haystack undismiss acme/widgets#99 # Undo for specific repo
419
451
  `)
420
452
  .action(undismissCommand);
453
+ program
454
+ .command('review [pr]')
455
+ .description('Trigger a fresh Haystack analysis for a PR')
456
+ .option('--no-wait', 'Exit after triggering instead of waiting for results')
457
+ .addHelpText('after', `
458
+ Re-run full Haystack analysis on a PR's current head.
459
+
460
+ Haystack analyzes a PR once when it's opened (or marked ready); later
461
+ pushes only get a resolution check against the existing findings. Use
462
+ this command when new commits deserve fresh detection, or any time you
463
+ want a second opinion.
464
+
465
+ When called without a PR identifier, uses the last submitted PR.
466
+
467
+ PR identifier formats:
468
+ (none) Last submitted PR
469
+ 123 PR number (uses current repo)
470
+ #123 PR number with hash
471
+ owner/repo#123 Fully qualified
472
+ https://github.com/owner/repo/pull/123 GitHub URL
473
+
474
+ Examples:
475
+ haystack review # Last submitted PR
476
+ haystack review 42 # Current repo, PR #42
477
+ haystack review acme/widgets#99 # Specific repo
478
+ haystack review 42 --no-wait # Trigger and exit
479
+ `)
480
+ .action(reviewCommand);
421
481
  program
422
482
  .command('request-review <pr> [reviewer]')
423
483
  .description('Tag a PR as needing human review')
@@ -447,7 +507,7 @@ program
447
507
  .option('--json', 'Output as JSON')
448
508
  .addHelpText('after', `
449
509
  Show what bucket a PR is in within the Haystack pipeline:
450
- analyzing, auto-fixing, good-to-merge, issues, needs-assignment, etc.
510
+ analyzing, good-to-merge, issues, needs-assignment, etc.
451
511
 
452
512
  PR identifier formats:
453
513
  123 PR number (uses current repo)
@@ -507,7 +567,7 @@ Examples:
507
567
  `)
508
568
  .action(handleAutoMerge);
509
569
  config
510
- .command('auto-fix [action]')
570
+ .command('auto-fix [action]', { hidden: true })
511
571
  .description('(Alpha) Auto-fix mechanical issues on PRs submitted via haystack submit (on|off|status)')
512
572
  .addHelpText('after', `
513
573
  Actions:
@@ -698,7 +758,7 @@ section when a PR touches matching files.
698
758
  // idempotency).
699
759
  const webhooks = program
700
760
  .command('webhooks')
701
- .description('Manage outbound webhooks (events: triage.completed, fixer.completed, ...)');
761
+ .description('Manage outbound webhooks');
702
762
  webhooks
703
763
  .command('register')
704
764
  .description('Register a webhook URL for the current repo and mint a fresh HMAC secret')
@@ -822,135 +882,6 @@ rules
822
882
  process.exit(1);
823
883
  }
824
884
  });
825
- // ─── verification ────────────────────────────────────────────────────────────
826
- //
827
- // Repo-local verification contract (.haystack/verification.yml): how to boot
828
- // the app, prove it's ready, log in as test personas, run scenarios, and
829
- // collect evidence into a standard artifact manifest + review packet.
830
- // See docs/VERIFICATION-CONTRACT.md.
831
- const verification = program
832
- .command('verification')
833
- .description('Repo verification contract — validate, run scenarios, collect evidence');
834
- verification
835
- .command('init')
836
- .description('Scaffold .haystack/verification.yml, preflight/smoke scripts, and the agent skill')
837
- .option('-f, --force', 'Overwrite existing files')
838
- .option('--skill', 'Only write the install-verification agent skill (hand the integration to a coding agent)')
839
- .addHelpText('after', `
840
- Creates:
841
- .haystack/verification.yml the verification contract
842
- scripts/haystack-preflight app-is-ready check
843
- scripts/haystack-smoke boot + capture-evidence smoke scenario
844
- .haystack/AGENTS.md how agents should use the contract
845
- .haystack/skills/install-verification/SKILL.md skill for completing the integration
846
-
847
- Detection fills in framework, package manager, dev command, port, and any
848
- auth-bypass env var found in the repo's own env examples. Review the output —
849
- detection is a starting point, not truth.
850
- `)
851
- .action(async (opts) => {
852
- try {
853
- await verificationInitCommand(opts);
854
- }
855
- catch (err) {
856
- console.error(chalk.red('init failed:'), err instanceof Error ? err.message : err);
857
- process.exit(1);
858
- }
859
- });
860
- verification
861
- .command('validate')
862
- .description('Check whether the repo satisfies the verification contract')
863
- .option('--json', 'Machine-readable report')
864
- .addHelpText('after', `
865
- Readiness verdicts:
866
- ready all checks pass (exit 0)
867
- partially_ready usable, with gaps (exit 0)
868
- not_ready contract missing/invalid or commands broken (exit 1)
869
- unsafe contract permits real side effects, or login helpers
870
- lack a production-disabled proof (exit 2)
871
- `)
872
- .action(async (opts) => {
873
- try {
874
- await verificationValidateCommand(opts);
875
- }
876
- catch (err) {
877
- console.error(chalk.red('validate failed:'), err instanceof Error ? err.message : err);
878
- process.exit(1);
879
- }
880
- });
881
- verification
882
- .command('preflight')
883
- .description('Run the contract preflight — is the app up and answering?')
884
- .action(async () => {
885
- try {
886
- await verificationPreflightCommand();
887
- }
888
- catch (err) {
889
- console.error(chalk.red('preflight failed:'), err instanceof Error ? err.message : err);
890
- process.exit(1);
891
- }
892
- });
893
- verification
894
- .command('run <scenario>')
895
- .description('Run a contract scenario and write the artifact manifest + review packet')
896
- .option('--json', 'Print the manifest as JSON')
897
- .option('--setup', 'Run environment.setup first (clean-clone mode)')
898
- .option('--skip-preflight', 'Skip the preflight check')
899
- .option('--timeout <seconds>', 'Override the scenario timeout', (v) => parseInt(v, 10))
900
- .addHelpText('after', `
901
- Runs preflight, then the scenario command, capturing stdout/stderr to the
902
- artifact directory. Ends with a status:
903
- verified scenario passed (exit 0)
904
- failed scenario failed (exit 1)
905
- environment_not_ready setup/preflight failed before the scenario ran (exit 1)
906
- unsafe_to_verify safety policy permits real side effects (exit 2)
907
-
908
- Evidence lands in the contract's artifact_dir:
909
- manifest-<scenario>.json machine-readable evidence manifest
910
- review-packet-<scenario>.md human-readable review packet
911
- logs/ captured command output
912
-
913
- Examples:
914
- haystack verification run smoke
915
- haystack verification run billing_upgrade --json
916
- haystack verification run smoke --setup # clean-clone mode (CI)
917
- `)
918
- .action(async (scenario, opts) => {
919
- try {
920
- await verificationRunCommand(scenario, opts);
921
- }
922
- catch (err) {
923
- console.error(chalk.red('run failed:'), err instanceof Error ? err.message : err);
924
- process.exit(1);
925
- }
926
- });
927
- verification
928
- .command('collect-artifacts')
929
- .description('Rebuild an evidence manifest from artifacts already on disk (status: inconclusive)')
930
- .option('--scenario <name>', 'Scenario name to record in the manifest')
931
- .option('--json', 'Print the manifest as JSON')
932
- .action(async (opts) => {
933
- try {
934
- await verificationCollectCommand(opts);
935
- }
936
- catch (err) {
937
- console.error(chalk.red('collect-artifacts failed:'), err instanceof Error ? err.message : err);
938
- process.exit(1);
939
- }
940
- });
941
- verification
942
- .command('check-safety')
943
- .description('Verify the verification setup is safe (policy, prod-disabled proof, dev-route gating, secret scan)')
944
- .option('--json', 'Machine-readable report')
945
- .action(async (opts) => {
946
- try {
947
- await verificationCheckSafetyCommand(opts);
948
- }
949
- catch (err) {
950
- console.error(chalk.red('check-safety failed:'), err instanceof Error ? err.message : err);
951
- process.exit(1);
952
- }
953
- });
954
885
  // ─── mcp ─────────────────────────────────────────────────────────────────────
955
886
  program
956
887
  .command('mcp')
package/dist/schema.d.ts CHANGED
@@ -10,9 +10,12 @@
10
10
  * uses the same envelope as the `--json` output, byte-for-byte.
11
11
  */
12
12
  export declare const SCHEMA_VERSIONS: {
13
- readonly triage: "1.0.0";
13
+ readonly triage: "2.0.0";
14
14
  readonly setup: "1.0.0";
15
- readonly pr: "1.0.0";
15
+ readonly pr: "2.0.0";
16
+ readonly inbox: "1.0.0";
17
+ readonly ask: "1.0.0";
18
+ readonly traces: "1.0.0";
16
19
  };
17
20
  export type SchemaName = keyof typeof SCHEMA_VERSIONS;
18
21
  /** Wrap a payload with the `schema_version` envelope. */
package/dist/schema.js CHANGED
@@ -10,9 +10,12 @@
10
10
  * uses the same envelope as the `--json` output, byte-for-byte.
11
11
  */
12
12
  export const SCHEMA_VERSIONS = {
13
- triage: '1.0.0',
13
+ triage: '2.0.0',
14
14
  setup: '1.0.0',
15
- pr: '1.0.0',
15
+ pr: '2.0.0',
16
+ inbox: '1.0.0',
17
+ ask: '1.0.0',
18
+ traces: '1.0.0',
16
19
  };
17
20
  /** Wrap a payload with the `schema_version` envelope. */
18
21
  export function withSchema(name, payload) {
@@ -44,6 +44,12 @@ export interface SynthesisDisplayIssue {
44
44
  detail: string;
45
45
  agentFixPrompt?: string;
46
46
  source?: string;
47
+ changeKey?: string;
48
+ filePath?: string;
49
+ startLine?: number;
50
+ endLine?: number;
51
+ severity?: 'critical' | 'high' | 'medium' | 'low';
52
+ mechanicalFix?: boolean;
47
53
  }
48
54
  export interface VerifiedBug {
49
55
  originalTitle: string;
@@ -104,6 +110,39 @@ export type AnalysisReadyResult = {
104
110
  status: 'error';
105
111
  message: string;
106
112
  };
113
+ export type TriggerAnalysisResult = {
114
+ status: 'started';
115
+ requestId: string;
116
+ } | {
117
+ status: 'already_processing';
118
+ requestId: string;
119
+ } | {
120
+ status: 'rate_limited';
121
+ message: string;
122
+ retryAfterSeconds?: number;
123
+ } | {
124
+ status: 'denied';
125
+ message: string;
126
+ } | {
127
+ status: 'error';
128
+ message: string;
129
+ };
130
+ /**
131
+ * Trigger a fresh full analysis for a PR via the auth-worker
132
+ * (`/api/analysis-trigger/{owner}/{repo}/{pr}`). Requires auth — triggers
133
+ * start a real analysis run. Returns the request id to wait on.
134
+ */
135
+ export declare function triggerAnalysis(owner: string, repo: string, prNumber: number, token: string, headSha?: string): Promise<TriggerAnalysisResult>;
136
+ export type TriggerRunStatus = 'pending' | 'COMPLETED' | 'FAILED' | 'SUPERSEDED' | 'error';
137
+ /**
138
+ * Poll the status of a triggered run by request id. The PR-level result
139
+ * endpoint can't distinguish a fresh re-run on the same head from the
140
+ * previous completed analysis — the request id can.
141
+ */
142
+ export declare function fetchTriggerStatus(requestId: string, token: string): Promise<{
143
+ status: TriggerRunStatus;
144
+ message?: string;
145
+ }>;
107
146
  /**
108
147
  * Check if analysis results are available for a PR by polling the result endpoint.
109
148
  * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
@@ -131,6 +170,7 @@ export interface AutoFixManifest {
131
170
  reason: string;
132
171
  }>;
133
172
  timestamp: string;
173
+ resultSha?: string;
134
174
  }
135
175
  export declare function fetchAutoFixManifest(owner: string, repo: string, prNumber: number, token?: string): Promise<AutoFixManifest | null>;
136
176
  export declare function fetchRatingSynthesis(owner: string, repo: string, prNumber: number, token?: string): Promise<RatingSynthesis | null>;
@@ -61,9 +61,114 @@ function apiHeaders(token) {
61
61
  }
62
62
  return headers;
63
63
  }
64
- // ============================================================================
65
- // API functions
66
- // ============================================================================
64
+ /**
65
+ * Trigger a fresh full analysis for a PR via the auth-worker
66
+ * (`/api/analysis-trigger/{owner}/{repo}/{pr}`). Requires auth — triggers
67
+ * start a real analysis run. Returns the request id to wait on.
68
+ */
69
+ export async function triggerAnalysis(owner, repo, prNumber, token, headSha) {
70
+ const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/${owner}/${repo}/${prNumber}`;
71
+ let response;
72
+ try {
73
+ // Force semantics ride on a server-set header at the trigger route.
74
+ // commitSha matters for dedup: without it, ingestion matches this
75
+ // request onto any in-flight run for an OLDER head and the CLI would
76
+ // wait on (and render) stale results.
77
+ response = await fetch(url, {
78
+ method: 'POST',
79
+ headers: { ...apiHeaders(token), 'Content-Type': 'application/json' },
80
+ body: JSON.stringify(headSha ? { commitSha: headSha } : {}),
81
+ });
82
+ }
83
+ catch (err) {
84
+ return { status: 'error', message: err.message };
85
+ }
86
+ let data = {};
87
+ let parseFailed = false;
88
+ try {
89
+ data = await response.json();
90
+ }
91
+ catch {
92
+ // Recorded, not swallowed: for non-OK responses the status-code handling
93
+ // below surfaces the failure; a 2xx with an unparseable body is its own
94
+ // explicit error (never "empty data") so callers can't mistake a
95
+ // malformed response for a normal one.
96
+ parseFailed = true;
97
+ }
98
+ if (parseFailed && response.ok) {
99
+ return { status: 'error', message: `Trigger endpoint returned invalid JSON (HTTP ${response.status})` };
100
+ }
101
+ if (response.status === 429) {
102
+ return {
103
+ status: 'rate_limited',
104
+ message: data.message || 'Too many analysis requests. Please wait before trying again.',
105
+ retryAfterSeconds: data.retryAfter,
106
+ };
107
+ }
108
+ if (response.status === 401 || response.status === 403) {
109
+ return {
110
+ status: 'denied',
111
+ message: data.message || data.error || `HTTP ${response.status}`,
112
+ };
113
+ }
114
+ if (!response.ok) {
115
+ return { status: 'error', message: data.message || data.error || `HTTP ${response.status}` };
116
+ }
117
+ if (!data.request_id) {
118
+ return { status: 'error', message: 'Trigger response did not include a request id' };
119
+ }
120
+ if (data.status === 'already_processing') {
121
+ return { status: 'already_processing', requestId: data.request_id };
122
+ }
123
+ return { status: 'started', requestId: data.request_id };
124
+ }
125
+ // Terminal states of the v3 status vocabulary (map_status_for_frontend in
126
+ // haystack-v3-request-status). Everything else — QUEUED, PROCESSING,
127
+ // CLONING, PREPARING, ANALYZING, not_found (row lag), and any state a newer
128
+ // backend introduces — reads as pending: misclassifying a healthy
129
+ // in-progress state as an error would abort the wait.
130
+ const TERMINAL_STATUSES = {
131
+ COMPLETED: 'COMPLETED',
132
+ FAILED: 'FAILED',
133
+ TIMED_OUT: 'FAILED',
134
+ SUPERSEDED: 'SUPERSEDED',
135
+ };
136
+ /**
137
+ * Poll the status of a triggered run by request id. The PR-level result
138
+ * endpoint can't distinguish a fresh re-run on the same head from the
139
+ * previous completed analysis — the request id can.
140
+ */
141
+ export async function fetchTriggerStatus(requestId, token) {
142
+ const url = `${ANALYSIS_API_BASE}/api/analysis-trigger/status/${requestId}`;
143
+ try {
144
+ const response = await fetch(url, { headers: apiHeaders(token) });
145
+ // The status Lambda returns not_found as a 404 WITH a JSON body — parse
146
+ // before rejecting on status code so "not yet written" is distinguishable
147
+ // from a real transport failure.
148
+ let data = {};
149
+ try {
150
+ data = await response.json();
151
+ }
152
+ catch {
153
+ return { status: 'error', message: `HTTP ${response.status}` };
154
+ }
155
+ const status = typeof data.status === 'string' ? data.status : '';
156
+ const terminal = TERMINAL_STATUSES[status];
157
+ if (terminal) {
158
+ return { status: terminal, message: data.message };
159
+ }
160
+ // Only successful responses (plus the documented 404 not_found row-lag
161
+ // case) read as pending — a 5xx carrying {"status":"error"} must count
162
+ // toward the error budget, not spin the wait for the full timeout.
163
+ if (response.ok || (response.status === 404 && status === 'not_found')) {
164
+ return { status: 'pending', message: data.message };
165
+ }
166
+ return { status: 'error', message: data.message || `HTTP ${response.status}` };
167
+ }
168
+ catch (err) {
169
+ return { status: 'error', message: err.message };
170
+ }
171
+ }
67
172
  /**
68
173
  * Check if analysis results are available for a PR by polling the result endpoint.
69
174
  * Returns 'ready' if complete, 'pending' if not yet available, or 'error' for server failures.
@@ -0,0 +1,20 @@
1
+ export interface UserInstallation {
2
+ id: number;
3
+ account?: {
4
+ login?: string;
5
+ } | null;
6
+ repositories?: Array<{
7
+ full_name: string;
8
+ pushed_at?: string | null;
9
+ }>;
10
+ }
11
+ export interface CliIdentity {
12
+ login: string;
13
+ id: number;
14
+ }
15
+ export declare function haystackJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
16
+ export declare function fetchCliIdentity(token: string): Promise<CliIdentity>;
17
+ export declare function fetchHaystackInstallations(token: string): Promise<UserInstallation[]>;
18
+ export declare function fetchGitHubJson<T>(path: string, token: string, init?: RequestInit): Promise<T>;
19
+ export declare function fetchAnalysisArtifact<T>(owner: string, repo: string, prNumber: number, file: string, token: string): Promise<T | null>;
20
+ export declare function postAskHaystack<T>(owner: string, repo: string, prNumber: number, question: string, sessionId: string | undefined, token: string): Promise<T>;
@@ -0,0 +1,66 @@
1
+ const HAYSTACK_API_BASE = process.env.HAYSTACK_API_BASE ?? 'https://haystackeditor.com';
2
+ function headers(token, extra) {
3
+ const result = new Headers(extra);
4
+ result.set('Authorization', `Bearer ${token}`);
5
+ result.set('Accept', 'application/json');
6
+ result.set('User-Agent', 'Haystack-CLI');
7
+ return result;
8
+ }
9
+ async function errorMessage(response) {
10
+ const text = await response.text().catch(() => '');
11
+ if (!text)
12
+ return `HTTP ${response.status}`;
13
+ try {
14
+ const parsed = JSON.parse(text);
15
+ return parsed.message ?? parsed.error ?? text;
16
+ }
17
+ catch {
18
+ return text;
19
+ }
20
+ }
21
+ export async function haystackJson(path, token, init = {}) {
22
+ const response = await fetch(`${HAYSTACK_API_BASE}${path}`, {
23
+ ...init,
24
+ headers: headers(token, init.headers),
25
+ });
26
+ if (!response.ok) {
27
+ throw new Error(`${response.status} ${await errorMessage(response)}`);
28
+ }
29
+ return response.json();
30
+ }
31
+ export async function fetchCliIdentity(token) {
32
+ return haystackJson('/api/cli/me', token);
33
+ }
34
+ export async function fetchHaystackInstallations(token) {
35
+ const data = await haystackJson('/api/github/user-installations', token);
36
+ return data.installations ?? [];
37
+ }
38
+ export async function fetchGitHubJson(path, token, init = {}) {
39
+ return haystackJson(`/api/github${path}`, token, init);
40
+ }
41
+ export async function fetchAnalysisArtifact(owner, repo, prNumber, file, token) {
42
+ const ref = encodeURIComponent(`${owner}/${repo}#${prNumber}`);
43
+ const response = await fetch(`${HAYSTACK_API_BASE}/api/analysis/${ref}/${file}`, {
44
+ headers: headers(token, { 'Cache-Control': 'no-cache' }),
45
+ });
46
+ if (response.status === 404)
47
+ return null;
48
+ if (!response.ok) {
49
+ throw new Error(`${response.status} ${await errorMessage(response)}`);
50
+ }
51
+ const data = await response.json();
52
+ if (!data || typeof data !== 'object' || !('download_url' in data) || !data.download_url) {
53
+ return data;
54
+ }
55
+ const download = await fetch(data.download_url, { headers: { 'User-Agent': 'Haystack-CLI' } });
56
+ if (!download.ok)
57
+ throw new Error(`Artifact download failed: HTTP ${download.status}`);
58
+ return download.json();
59
+ }
60
+ export async function postAskHaystack(owner, repo, prNumber, question, sessionId, token) {
61
+ return haystackJson(`/api/review-chat/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${prNumber}/ask`, token, {
62
+ method: 'POST',
63
+ headers: { 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ question, session_id: sessionId }),
65
+ });
66
+ }
package/package.json CHANGED
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.17",
3
+ "version": "0.15.19",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "haystack": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc && rm -rf dist/assets && cp -r src/assets dist/assets",
10
+ "build": "rm -rf dist && tsc && cp -r src/assets dist/assets",
11
11
  "dev": "tsc --watch",
12
12
  "start": "node dist/index.js",
13
13
  "check:schemas": "node scripts/check-schemas.mjs",
14
14
  "release": "node scripts/release.mjs",
15
15
  "prepublishOnly": "node scripts/no-direct-publish.mjs",
16
- "test": "vitest run"
16
+ "test": "pnpm build && vitest run && node --test tests/*.test.mjs"
17
17
  },
18
18
  "keywords": [
19
19
  "haystack",
@@ -41,7 +41,7 @@
41
41
  "fast-glob": "3.3.3",
42
42
  "glob": "10.5.0",
43
43
  "inquirer": "9.3.8",
44
- "ws": "8.19.0",
44
+ "ws": "8.21.0",
45
45
  "yaml": "2.8.2",
46
46
  "zod": "3.25.76"
47
47
  },