@haystackeditor/cli 0.15.18 → 0.15.20

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 (53) hide show
  1. package/README.md +86 -1
  2. package/dist/assets/skills/map-your-system.md +143 -0
  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 +25 -0
  17. package/dist/commands/review.js +191 -0
  18. package/dist/commands/setup.js +4 -1
  19. package/dist/commands/skills.js +4 -0
  20. package/dist/commands/submit.d.ts +3 -0
  21. package/dist/commands/submit.js +178 -38
  22. package/dist/commands/system-map.d.ts +42 -0
  23. package/dist/commands/system-map.js +228 -0
  24. package/dist/commands/tokens.js +3 -2
  25. package/dist/commands/traces.js +2 -2
  26. package/dist/commands/triage.d.ts +1 -6
  27. package/dist/commands/triage.js +51 -55
  28. package/dist/index.js +116 -19
  29. package/dist/schema.d.ts +4 -0
  30. package/dist/schema.js +4 -0
  31. package/dist/states.d.ts +29 -0
  32. package/dist/states.js +46 -0
  33. package/dist/triage/runner.d.ts +7 -0
  34. package/dist/triage/runner.js +2 -2
  35. package/dist/utils/action-output.d.ts +24 -0
  36. package/dist/utils/action-output.js +26 -0
  37. package/dist/utils/analysis-api.d.ts +44 -0
  38. package/dist/utils/analysis-api.js +137 -6
  39. package/dist/utils/auth.js +7 -0
  40. package/dist/utils/git.d.ts +4 -1
  41. package/dist/utils/git.js +17 -7
  42. package/dist/utils/haystack-api.d.ts +15 -0
  43. package/dist/utils/haystack-api.js +58 -7
  44. package/dist/utils/pr-ref.d.ts +27 -0
  45. package/dist/utils/pr-ref.js +56 -0
  46. package/package.json +1 -1
  47. package/schemas/action.v1.json +22 -0
  48. package/schemas/error.v1.json +14 -0
  49. package/schemas/pr-status.v1.json +61 -0
  50. package/schemas/pr.v1.json +80 -17
  51. package/schemas/setup.v1.json +66 -12
  52. package/schemas/submit.v1.json +88 -0
  53. 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
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * review command — Trigger a fresh Haystack analysis for a PR.
3
+ *
4
+ * haystack review [pr] [--no-wait]
5
+ *
6
+ * Haystack runs a full analysis when a PR is opened (or marked ready);
7
+ * later pushes only get a resolution check against the existing findings.
8
+ * This command is the explicit escape hatch: it re-runs full detection on
9
+ * the PR's current head — use it after substantial new commits, or any
10
+ * time you want a fresh second opinion.
11
+ *
12
+ * When called without a PR identifier, reads the last submitted PR from
13
+ * `.haystack/pending-submit.json` (written by `haystack submit`).
14
+ *
15
+ * Accepts a PR identifier in several formats:
16
+ * haystack review # Last submitted PR
17
+ * haystack review 123 # Uses current repo's owner/repo
18
+ * haystack review owner/repo#123 # Fully qualified
19
+ * haystack review https://github.com/owner/repo/pull/123
20
+ */
21
+ export interface ReviewOptions {
22
+ wait?: boolean;
23
+ json?: boolean;
24
+ }
25
+ export declare function reviewCommand(identifier: string | undefined, options: ReviewOptions): Promise<void>;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * review command — Trigger a fresh Haystack analysis for a PR.
3
+ *
4
+ * haystack review [pr] [--no-wait]
5
+ *
6
+ * Haystack runs a full analysis when a PR is opened (or marked ready);
7
+ * later pushes only get a resolution check against the existing findings.
8
+ * This command is the explicit escape hatch: it re-runs full detection on
9
+ * the PR's current head — use it after substantial new commits, or any
10
+ * time you want a fresh second opinion.
11
+ *
12
+ * When called without a PR identifier, reads the last submitted PR from
13
+ * `.haystack/pending-submit.json` (written by `haystack submit`).
14
+ *
15
+ * Accepts a PR identifier in several formats:
16
+ * haystack review # Last submitted PR
17
+ * haystack review 123 # Uses current repo's owner/repo
18
+ * haystack review owner/repo#123 # Fully qualified
19
+ * haystack review https://github.com/owner/repo/pull/123
20
+ */
21
+ import chalk from 'chalk';
22
+ import { emitActionPayload, failAction } from '../utils/action-output.js';
23
+ import { triggerAnalysis, fetchTriggerStatus } from '../utils/analysis-api.js';
24
+ import { fetchTriageHeadSha } from './triage.js';
25
+ import { loadToken } from '../utils/auth.js';
26
+ import { loadPendingSubmit } from '../utils/pending-state.js';
27
+ import { parsePrRef } from '../utils/pr-ref.js';
28
+ import { triageCommand } from './triage.js';
29
+ // ============================================================================
30
+ // Constants
31
+ // ============================================================================
32
+ const STATUS_POLL_INTERVAL_MS = 10_000;
33
+ // Full analyses regularly take 10+ minutes on large PRs; this bounds the
34
+ // CLI's wait, not the analysis itself (results land on the feed regardless).
35
+ const STATUS_POLL_TIMEOUT_MS = 25 * 60 * 1000;
36
+ function resolvePR(identifier, json) {
37
+ if (identifier) {
38
+ try {
39
+ return parsePrRef(identifier, 'review');
40
+ }
41
+ catch (err) {
42
+ failAction(json, 'trigger_review', null, err.message);
43
+ }
44
+ }
45
+ const pending = loadPendingSubmit();
46
+ if (!pending) {
47
+ console.log(chalk.dim('No pending submits. Pass a PR number to review a specific PR.\n'));
48
+ return null;
49
+ }
50
+ return { owner: pending.owner, repo: pending.repo, prNumber: pending.prNumber };
51
+ }
52
+ // ============================================================================
53
+ // Waiting
54
+ // ============================================================================
55
+ async function waitForRun(requestId, token) {
56
+ const deadline = Date.now() + STATUS_POLL_TIMEOUT_MS;
57
+ let consecutiveErrors = 0;
58
+ while (Date.now() < deadline) {
59
+ const result = await fetchTriggerStatus(requestId, token);
60
+ if (result.status === 'COMPLETED')
61
+ return 'completed';
62
+ if (result.status === 'FAILED')
63
+ return 'failed';
64
+ if (result.status === 'SUPERSEDED')
65
+ return 'superseded';
66
+ if (result.status === 'error') {
67
+ consecutiveErrors++;
68
+ if (consecutiveErrors >= 5)
69
+ return 'failed';
70
+ }
71
+ else {
72
+ consecutiveErrors = 0;
73
+ }
74
+ process.stderr.write(`\r ${chalk.dim('Analysis in progress...')}${' '.repeat(5)}`);
75
+ // Deliberately NOT unref'd: during the wait this timer can be the only
76
+ // live handle, and an unref'd timer lets Node exit mid-wait.
77
+ await new Promise((resolve) => {
78
+ setTimeout(resolve, STATUS_POLL_INTERVAL_MS);
79
+ });
80
+ }
81
+ return 'timeout';
82
+ }
83
+ // ============================================================================
84
+ // Main command
85
+ // ============================================================================
86
+ export async function reviewCommand(identifier, options) {
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);
91
+ if (!pr)
92
+ return;
93
+ const prLabel = `${pr.owner}/${pr.repo}#${pr.prNumber}`;
94
+ let token = null;
95
+ try {
96
+ token = await loadToken({ owner: pr.owner, repo: pr.repo });
97
+ }
98
+ catch (err) {
99
+ failAction(options.json, 'trigger_review', prLabel, `Could not load Haystack credentials: ${err.message}`);
100
+ }
101
+ if (!token) {
102
+ failAction(options.json, 'trigger_review', prLabel, 'Triggering an analysis requires authentication. Run `haystack login` first.');
103
+ return; // unreachable — satisfies TS narrowing
104
+ }
105
+ if (!options.json)
106
+ console.log(chalk.dim(`\nRequesting a fresh analysis for ${prLabel}...`));
107
+ // Resolve the current head so ingestion's dedup compares commits — an
108
+ // in-flight run for an OLDER head must be superseded, not reused. A
109
+ // failed metadata fetch degrades to a head-less trigger (the dedup is a
110
+ // freshness aid, not a correctness requirement) instead of crashing.
111
+ let headSha;
112
+ try {
113
+ headSha = await fetchTriageHeadSha(pr, token) ?? undefined;
114
+ }
115
+ catch (err) {
116
+ console.error(chalk.dim(` Could not resolve the PR head (${err.message}) — continuing without it.`));
117
+ }
118
+ const trigger = await triggerAnalysis(pr.owner, pr.repo, pr.prNumber, token, headSha);
119
+ if (trigger.status === 'rate_limited') {
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}`);
124
+ }
125
+ if (trigger.status === 'denied') {
126
+ failAction(options.json, 'trigger_review', prLabel, trigger.message);
127
+ }
128
+ if (trigger.status === 'error') {
129
+ failAction(options.json, 'trigger_review', prLabel, `Failed to trigger analysis: ${trigger.message}`);
130
+ return; // unreachable — satisfies TS narrowing
131
+ }
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
+ }
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
+ };
152
+ if (options.wait === false) {
153
+ if (options.json) {
154
+ emitJson(trigger.status, 'Analysis triggered; not waiting for completion.', true);
155
+ return;
156
+ }
157
+ console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${prLabel}`));
158
+ console.log(chalk.dim(' Or view the review: ') + chalk.cyan(`https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}\n`));
159
+ return;
160
+ }
161
+ const outcome = await waitForRun(trigger.requestId, token);
162
+ process.stderr.write('\r' + ' '.repeat(60) + '\r');
163
+ if (outcome === 'failed') {
164
+ failAction(options.json, 'trigger_review', prLabel, `Analysis run failed. Check the review page: https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`);
165
+ }
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
+ }
171
+ console.log(chalk.yellow('\n This run was superseded by a newer analysis (a push likely landed meanwhile).'));
172
+ console.log(chalk.dim(' Showing the latest results instead.'));
173
+ await triageCommand(prLabel, { wait: false });
174
+ return;
175
+ }
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
+ }
181
+ console.log(chalk.yellow('\n Still running after 25 minutes — giving up on waiting.'));
182
+ console.log(chalk.dim(' Check results later with: ') + chalk.cyan(`haystack triage ${prLabel}\n`));
183
+ return;
184
+ }
185
+ // Completed — render results through the triage view (no extra wait).
186
+ if (options.json) {
187
+ emitJson('completed', 'Analysis completed.', true);
188
+ return;
189
+ }
190
+ await triageCommand(prLabel, { wait: false });
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))
@@ -15,6 +15,7 @@ function getSkillsSourceDir() {
15
15
  const SKILL_FILES = [
16
16
  'setup-haystack.md',
17
17
  'submit.md',
18
+ 'map-your-system.md',
18
19
  ];
19
20
  function installSkillFiles(targetDir) {
20
21
  const sourceDir = getSkillsSourceDir();
@@ -55,6 +56,7 @@ export async function installSkills(options) {
55
56
  }
56
57
  const claudeCommandPath = join(claudeCommandsDir, 'setup-haystack.md');
57
58
  writeFileSync(claudeCommandPath, '# Set Up Haystack\n\nFollow .agents/skills/setup-haystack.md to set up Haystack for this repo.\n');
59
+ writeFileSync(join(claudeCommandsDir, 'map-your-system.md'), '# Map Your System for Haystack QA\n\nFollow .agents/skills/map-your-system.md to create .haystack/system.yml — verified facts about how this system builds, boots, seeds data, and logs in, so Haystack QA can run it.\n');
58
60
  showSuccessMessage();
59
61
  }
60
62
  function showSuccessMessage() {
@@ -62,6 +64,7 @@ function showSuccessMessage() {
62
64
  console.log(chalk.white('Available skills:'));
63
65
  console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Create .haystack.json with AI assistance'));
64
66
  console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
67
+ console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
65
68
  console.log();
66
69
  console.log(chalk.white('Run in your coding CLI:'));
67
70
  console.log(chalk.cyan(' /setup-haystack'));
@@ -71,5 +74,6 @@ export async function listSkills() {
71
74
  console.log(chalk.white('\nAvailable Haystack skills:\n'));
72
75
  console.log(chalk.cyan(' /setup-haystack ') + chalk.gray('- Master setup - creates .haystack.json'));
73
76
  console.log(chalk.cyan(' /submit ') + chalk.gray('- Submit a PR via Haystack'));
77
+ console.log(chalk.cyan(' /map-your-system ') + chalk.gray('- Write .haystack/system.yml so Haystack QA can run this system'));
74
78
  console.log();
75
79
  }
@@ -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
  }