@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
@@ -12,4 +12,15 @@
12
12
  * owner/repo#123 # Fully qualified
13
13
  * https://github.com/owner/repo/pull/123 # GitHub URL
14
14
  */
15
- export declare function requestReviewCommand(identifier: string, reviewer?: string): Promise<void>;
15
+ import { type ActionPayload } from '../utils/action-output.js';
16
+ export interface RequestReviewPayload extends ActionPayload {
17
+ reviewer: string | null;
18
+ /** Set when the label was applied but the specific-reviewer request failed. */
19
+ reviewer_request_failed: string | null;
20
+ }
21
+ /** Apply the needs-review label (and optional reviewer). Throws on failure. */
22
+ export declare function performRequestReview(identifier: string, reviewer?: string): Promise<RequestReviewPayload>;
23
+ export interface RequestReviewOptions {
24
+ json?: boolean;
25
+ }
26
+ export declare function requestReviewCommand(identifier: string, reviewer?: string, options?: RequestReviewOptions): Promise<void>;
@@ -13,85 +13,62 @@
13
13
  * https://github.com/owner/repo/pull/123 # GitHub URL
14
14
  */
15
15
  import chalk from 'chalk';
16
+ import { emitActionPayload, failAction } from '../utils/action-output.js';
16
17
  import { resolveAuthContext } from '../utils/auth.js';
17
- import { parseRemoteUrl } from '../utils/git.js';
18
+ import { parsePrRef } from '../utils/pr-ref.js';
18
19
  import { ensureHaystackLabels, addLabelsToIssue, requestReviewers, } from '../utils/github-api.js';
19
- // ============================================================================
20
- // PR identifier parsing (shared pattern with dismiss.ts)
21
- // ============================================================================
22
- function parsePRIdentifier(identifier) {
23
- const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
24
- if (urlMatch) {
25
- return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
26
- }
27
- const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
28
- if (qualifiedMatch) {
29
- return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
30
- }
31
- const numberMatch = identifier.match(/^#?(\d+)$/);
32
- if (numberMatch) {
33
- const prNumber = parseInt(numberMatch[1], 10);
20
+ /** Apply the needs-review label (and optional reviewer). Throws on failure. */
21
+ export async function performRequestReview(identifier, reviewer) {
22
+ const pr = parsePrRef(identifier, 'request-review');
23
+ const authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
24
+ const ref = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
25
+ await ensureHaystackLabels(pr.owner, pr.repo, ['haystack:needs-review'], authContext.token);
26
+ await addLabelsToIssue(pr.owner, pr.repo, pr.prNumber, ['haystack:needs-review'], authContext.token);
27
+ let reviewerRequestFailed = null;
28
+ if (reviewer) {
34
29
  try {
35
- const remote = parseRemoteUrl('origin');
36
- return { owner: remote.owner, repo: remote.repo, prNumber };
30
+ await requestReviewers(pr.owner, pr.repo, pr.prNumber, [reviewer], authContext.token);
37
31
  }
38
- catch {
39
- throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
40
- `Use the full format: haystack request-review owner/repo#${prNumber}`);
32
+ catch (err) {
33
+ // Label was added successfully, just the reviewer request failed —
34
+ // report the partial outcome instead of failing the whole action.
35
+ reviewerRequestFailed = err.message;
41
36
  }
42
37
  }
43
- throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
44
- `Accepted formats:\n` +
45
- ` haystack request-review 123\n` +
46
- ` haystack request-review owner/repo#123\n` +
47
- ` haystack request-review https://github.com/owner/repo/pull/123`);
38
+ return {
39
+ action: 'request_review',
40
+ status: 'ok',
41
+ ref,
42
+ reviewer: reviewer && !reviewerRequestFailed ? reviewer : null,
43
+ reviewer_request_failed: reviewerRequestFailed,
44
+ detail: reviewer && !reviewerRequestFailed
45
+ ? `Review requested from ${reviewer}; the PR moves to the "Needs Assignment" queue.`
46
+ : 'PR marked for human review; it moves to the "Needs Assignment" queue.',
47
+ };
48
48
  }
49
- // ============================================================================
50
- // Main command
51
- // ============================================================================
52
- export async function requestReviewCommand(identifier, reviewer) {
53
- // Parse PR identifier
54
- let pr;
49
+ export async function requestReviewCommand(identifier, reviewer, options = {}) {
50
+ if (!options.json)
51
+ console.log(chalk.dim(`\nRequesting review for ${identifier}...`));
52
+ let payload;
55
53
  try {
56
- pr = parsePRIdentifier(identifier);
54
+ payload = await performRequestReview(identifier, reviewer);
57
55
  }
58
56
  catch (err) {
59
- console.error(chalk.red(`\n${err.message}\n`));
60
- process.exit(1);
57
+ failAction(options.json, 'request_review', null, err.message);
58
+ return; // unreachable — satisfies TS narrowing
61
59
  }
62
- let authContext;
63
- try {
64
- authContext = await resolveAuthContext({ owner: pr.owner, repo: pr.repo });
60
+ if (options.json) {
61
+ emitActionPayload(payload);
62
+ return;
65
63
  }
66
- catch (err) {
67
- console.error(chalk.red(`\n${err.message}\n`));
68
- process.exit(1);
69
- }
70
- const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
71
- console.log(chalk.dim(`\nRequesting review for ${prLabel}...`));
72
- // Ensure the label exists in the repo, then add it to the PR
73
- try {
74
- await ensureHaystackLabels(pr.owner, pr.repo, ['haystack:needs-review'], authContext.token);
75
- await addLabelsToIssue(pr.owner, pr.repo, pr.prNumber, ['haystack:needs-review'], authContext.token);
76
- }
77
- catch (err) {
78
- console.error(chalk.red(`\nFailed to add label: ${err.message}\n`));
79
- process.exit(1);
80
- }
81
- // Request specific reviewer if provided
82
- if (reviewer) {
83
- try {
84
- await requestReviewers(pr.owner, pr.repo, pr.prNumber, [reviewer], authContext.token);
85
- console.log(chalk.green(`\n Review requested from ${reviewer} on ${prLabel}`));
86
- }
87
- catch (err) {
88
- // Label was added successfully, just the reviewer request failed
89
- console.log(chalk.green(`\n Marked ${prLabel} for review`));
90
- console.log(chalk.yellow(` Could not request reviewer "${reviewer}": ${err.message}`));
91
- }
64
+ if (payload.reviewer) {
65
+ console.log(chalk.green(`\n Review requested from ${payload.reviewer} on ${payload.ref}`));
92
66
  }
93
67
  else {
94
- console.log(chalk.green(`\n Marked ${prLabel} for human review`));
68
+ console.log(chalk.green(`\n Marked ${payload.ref} for human review`));
69
+ if (payload.reviewer_request_failed) {
70
+ console.log(chalk.yellow(` Could not request reviewer "${reviewer}": ${payload.reviewer_request_failed}`));
71
+ }
95
72
  }
96
73
  console.log(chalk.dim(' The PR will appear in the "Needs Assignment" queue in the feed.\n'));
97
74
  }
@@ -20,5 +20,6 @@
20
20
  */
21
21
  export interface ReviewOptions {
22
22
  wait?: boolean;
23
+ json?: boolean;
23
24
  }
24
25
  export declare function reviewCommand(identifier: string | undefined, options: ReviewOptions): Promise<void>;
@@ -19,11 +19,12 @@
19
19
  * haystack review https://github.com/owner/repo/pull/123
20
20
  */
21
21
  import chalk from 'chalk';
22
+ import { emitActionPayload, failAction } from '../utils/action-output.js';
22
23
  import { triggerAnalysis, fetchTriggerStatus } from '../utils/analysis-api.js';
23
24
  import { fetchTriageHeadSha } from './triage.js';
24
- import { parseRemoteUrl } from '../utils/git.js';
25
25
  import { loadToken } from '../utils/auth.js';
26
26
  import { loadPendingSubmit } from '../utils/pending-state.js';
27
+ import { parsePrRef } from '../utils/pr-ref.js';
27
28
  import { triageCommand } from './triage.js';
28
29
  // ============================================================================
29
30
  // Constants
@@ -32,44 +33,13 @@ const STATUS_POLL_INTERVAL_MS = 10_000;
32
33
  // Full analyses regularly take 10+ minutes on large PRs; this bounds the
33
34
  // CLI's wait, not the analysis itself (results land on the feed regardless).
34
35
  const STATUS_POLL_TIMEOUT_MS = 25 * 60 * 1000;
35
- // ============================================================================
36
- // PR identifier parsing (shared pattern with request-review.ts / triage.ts)
37
- // ============================================================================
38
- function parsePRIdentifier(identifier) {
39
- const urlMatch = identifier.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/);
40
- if (urlMatch) {
41
- return { owner: urlMatch[1], repo: urlMatch[2], prNumber: parseInt(urlMatch[3], 10) };
42
- }
43
- const qualifiedMatch = identifier.match(/^([^/]+)\/([^#]+)#(\d+)$/);
44
- if (qualifiedMatch) {
45
- return { owner: qualifiedMatch[1], repo: qualifiedMatch[2], prNumber: parseInt(qualifiedMatch[3], 10) };
46
- }
47
- const numberMatch = identifier.match(/^#?(\d+)$/);
48
- if (numberMatch) {
49
- const prNumber = parseInt(numberMatch[1], 10);
50
- try {
51
- const remote = parseRemoteUrl('origin');
52
- return { owner: remote.owner, repo: remote.repo, prNumber };
53
- }
54
- catch {
55
- throw new Error(`Cannot determine repository for PR #${prNumber}.\n` +
56
- `Use the full format: haystack review owner/repo#${prNumber}`);
57
- }
58
- }
59
- throw new Error(`Invalid PR identifier: "${identifier}"\n\n` +
60
- `Accepted formats:\n` +
61
- ` haystack review 123\n` +
62
- ` haystack review owner/repo#123\n` +
63
- ` haystack review https://github.com/owner/repo/pull/123`);
64
- }
65
- function resolvePR(identifier) {
36
+ function resolvePR(identifier, json) {
66
37
  if (identifier) {
67
38
  try {
68
- return parsePRIdentifier(identifier);
39
+ return parsePrRef(identifier, 'review');
69
40
  }
70
41
  catch (err) {
71
- console.error(chalk.red(`\n${err.message}\n`));
72
- process.exit(1);
42
+ failAction(json, 'trigger_review', null, err.message);
73
43
  }
74
44
  }
75
45
  const pending = loadPendingSubmit();
@@ -114,7 +84,10 @@ async function waitForRun(requestId, token) {
114
84
  // Main command
115
85
  // ============================================================================
116
86
  export async function reviewCommand(identifier, options) {
117
- const pr = resolvePR(identifier);
87
+ if (options.json && identifier === undefined && !loadPendingSubmit()) {
88
+ failAction(true, 'trigger_review', null, 'No pending submit to review. Pass a PR identifier: haystack review owner/repo#123 --json');
89
+ }
90
+ const pr = resolvePR(identifier, options.json);
118
91
  if (!pr)
119
92
  return;
120
93
  const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
@@ -123,15 +96,14 @@ export async function reviewCommand(identifier, options) {
123
96
  token = await loadToken({ owner: pr.owner, repo: pr.repo });
124
97
  }
125
98
  catch (err) {
126
- console.error(chalk.red(`\nCould not load Haystack credentials: ${err.message}\n`));
127
- process.exit(1);
99
+ failAction(options.json, 'trigger_review', prLabel, `Could not load Haystack credentials: ${err.message}`);
128
100
  }
129
101
  if (!token) {
130
- console.error(chalk.red('\nTriggering an analysis requires authentication.'));
131
- console.log(chalk.dim(' Run ') + chalk.cyan('haystack login') + chalk.dim(' first.\n'));
132
- process.exit(1);
102
+ failAction(options.json, 'trigger_review', prLabel, 'Triggering an analysis requires authentication. Run `haystack login` first.');
103
+ return; // unreachable — satisfies TS narrowing
133
104
  }
134
- console.log(chalk.dim(`\nRequesting a fresh analysis for ${prLabel}...`));
105
+ if (!options.json)
106
+ console.log(chalk.dim(`\nRequesting a fresh analysis for ${prLabel}...`));
135
107
  // Resolve the current head so ingestion's dedup compares commits — an
136
108
  // in-flight run for an OLDER head must be superseded, not reused. A
137
109
  // failed metadata fetch degrades to a head-less trigger (the dedup is a
@@ -141,31 +113,47 @@ export async function reviewCommand(identifier, options) {
141
113
  headSha = await fetchTriageHeadSha(pr, token) ?? undefined;
142
114
  }
143
115
  catch (err) {
144
- console.log(chalk.dim(` Could not resolve the PR head (${err.message}) — continuing without it.`));
116
+ console.error(chalk.dim(` Could not resolve the PR head (${err.message}) — continuing without it.`));
145
117
  }
146
118
  const trigger = await triggerAnalysis(pr.owner, pr.repo, pr.prNumber, token, headSha);
147
119
  if (trigger.status === 'rate_limited') {
148
- console.error(chalk.red(`\n ${trigger.message}`));
149
- if (trigger.retryAfterSeconds) {
150
- console.log(chalk.dim(` Try again in ~${Math.ceil(trigger.retryAfterSeconds / 60)} minute(s).\n`));
151
- }
152
- process.exit(1);
120
+ const retryHint = trigger.retryAfterSeconds
121
+ ? ` Try again in ~${Math.ceil(trigger.retryAfterSeconds / 60)} minute(s).`
122
+ : '';
123
+ failAction(options.json, 'trigger_review', prLabel, `${trigger.message}${retryHint}`);
153
124
  }
154
125
  if (trigger.status === 'denied') {
155
- console.error(chalk.red(`\n ${trigger.message}\n`));
156
- process.exit(1);
126
+ failAction(options.json, 'trigger_review', prLabel, trigger.message);
157
127
  }
158
128
  if (trigger.status === 'error') {
159
- console.error(chalk.red(`\n Failed to trigger analysis: ${trigger.message}\n`));
160
- process.exit(1);
161
- }
162
- if (trigger.status === 'already_processing') {
163
- console.log(chalk.yellow(' An analysis for this PR is already running — waiting on it instead.'));
129
+ failAction(options.json, 'trigger_review', prLabel, `Failed to trigger analysis: ${trigger.message}`);
130
+ return; // unreachable — satisfies TS narrowing
164
131
  }
165
- else {
166
- console.log(chalk.green(' Analysis started.'));
132
+ if (!options.json) {
133
+ if (trigger.status === 'already_processing') {
134
+ console.log(chalk.yellow(' An analysis for this PR is already running — waiting on it instead.'));
135
+ }
136
+ else {
137
+ console.log(chalk.green(' Analysis started.'));
138
+ }
167
139
  }
140
+ /** One envelope for every terminal state of the trigger. */
141
+ const emitJson = (runStatus, detail, done) => {
142
+ emitActionPayload({
143
+ action: 'trigger_review',
144
+ status: 'ok',
145
+ ref: prLabel,
146
+ detail,
147
+ request_id: trigger.requestId,
148
+ run_status: runStatus,
149
+ next: done ? `haystack triage ${prLabel} --json` : null,
150
+ });
151
+ };
168
152
  if (options.wait === false) {
153
+ if (options.json) {
154
+ emitJson(trigger.status, 'Analysis triggered; not waiting for completion.', true);
155
+ return;
156
+ }
169
157
  console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${prLabel}`));
170
158
  console.log(chalk.dim(' Or view the review: ') + chalk.cyan(`https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
171
159
  return;
@@ -173,21 +161,31 @@ export async function reviewCommand(identifier, options) {
173
161
  const outcome = await waitForRun(trigger.requestId, token);
174
162
  process.stderr.write('\r' + ' '.repeat(60) + '\r');
175
163
  if (outcome === 'failed') {
176
- console.error(chalk.red('\n Analysis run failed.'));
177
- console.log(chalk.dim(' Check the review page: ') + chalk.cyan(`https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
178
- process.exit(1);
164
+ failAction(options.json, 'trigger_review', prLabel, `Analysis run failed. Check the review page: https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`);
179
165
  }
180
166
  if (outcome === 'superseded') {
167
+ if (options.json) {
168
+ emitJson('superseded', 'This run was superseded by a newer analysis (a push likely landed meanwhile). The latest results stand.', true);
169
+ return;
170
+ }
181
171
  console.log(chalk.yellow('\n This run was superseded by a newer analysis (a push likely landed meanwhile).'));
182
172
  console.log(chalk.dim(' Showing the latest results instead.'));
183
173
  await triageCommand(prLabel, { wait: false });
184
174
  return;
185
175
  }
186
176
  if (outcome === 'timeout') {
177
+ if (options.json) {
178
+ emitJson('running', 'Still running after 25 minutes; the analysis continues server-side.', true);
179
+ return;
180
+ }
187
181
  console.log(chalk.yellow('\n Still running after 25 minutes — giving up on waiting.'));
188
182
  console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${prLabel}\n`));
189
183
  return;
190
184
  }
191
185
  // Completed — render results through the triage view (no extra wait).
186
+ if (options.json) {
187
+ emitJson('completed', 'Analysis completed.', true);
188
+ return;
189
+ }
192
190
  await triageCommand(prLabel, { wait: false });
193
191
  }
@@ -48,8 +48,11 @@ function buildAnswerMap(options) {
48
48
  answers.want_to_toggle = false;
49
49
  if (!('confirm_write' in answers))
50
50
  answers.confirm_write = true;
51
- if (!('auto_merge' in answers))
51
+ if (!('auto_merge' in answers)) {
52
52
  answers.auto_merge = true;
53
+ // Consequential default applied without an interactive prompt — say so.
54
+ console.error('--yes: enabling auto-merge (the wizard default). Pass --no-auto-merge to opt out.');
55
+ }
53
56
  if (!('install_entire' in answers))
54
57
  answers.install_entire = true;
55
58
  if (!('upgrade_entire' in answers))
@@ -19,8 +19,11 @@ export interface SubmitOptions {
19
19
  review?: string | true;
20
20
  force?: boolean;
21
21
  wait?: boolean;
22
+ json?: boolean;
22
23
  autoFix?: boolean;
23
24
  autoMerge?: boolean;
25
+ autoMergeSource?: 'flag' | 'config';
26
+ autoFixSource?: 'flag' | 'config';
24
27
  maxTurns?: number;
25
28
  triageTimeout?: number;
26
29
  }