@haystackeditor/cli 0.15.6 → 0.15.7

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.
@@ -56,6 +56,10 @@ function buildAnswerMap(options) {
56
56
  answers.upgrade_entire = true;
57
57
  if (!('install_hooks' in answers))
58
58
  answers.install_hooks = true;
59
+ // Don't auto-launch a browser in an unattended --yes run; the feed URL is
60
+ // still printed and returned in the result for the caller to open.
61
+ if (!('open_feed' in answers))
62
+ answers.open_feed = false;
59
63
  }
60
64
  return answers;
61
65
  }
@@ -1120,11 +1124,23 @@ async function runSetupFlow(options) {
1120
1124
  else {
1121
1125
  await stepInstallEntire(selectedRepos, token, bootstrapPRs);
1122
1126
  }
1127
+ // Close the loop: point the user to their Haystack feed (the inbox/card stack
1128
+ // where their PRs surface). Interactive users get offered a browser open; an
1129
+ // agent gets the URL in the `result` event to surface/open as it sees fit.
1130
+ const feedUrl = `${HAYSTACK_API}/inbox`;
1131
+ console.log(chalk.bold('\n Your Haystack feed:'));
1132
+ console.log(` ${chalk.cyan(feedUrl)}\n`);
1133
+ if (ui.interactive) {
1134
+ const openFeed = await ui.confirm({ id: 'open_feed', message: 'Open your Haystack feed now?', default: true });
1135
+ if (openFeed)
1136
+ tryOpenBrowser(feedUrl);
1137
+ }
1123
1138
  // Final structured outcome for non-interactive/agent callers.
1124
1139
  ui.result({
1125
1140
  status: 'complete',
1126
1141
  repos: selectedRepos,
1127
1142
  pullRequests: [...bootstrapPRs.entries()].map(([repo, pr]) => ({ repo, url: pr.prUrl })),
1143
+ feedUrl,
1128
1144
  });
1129
1145
  }
1130
1146
  // =============================================================================
@@ -68,20 +68,54 @@ function parsePRIdentifier(identifier) {
68
68
  // ============================================================================
69
69
  // Output formatting
70
70
  // ============================================================================
71
+ /**
72
+ * Decide whether a finding is one the auto-fixer is already handling (and thus
73
+ * shouldn't be re-fixed). The fixer re-derives — often paraphrases — its manifest
74
+ * summaries rather than copying them verbatim, so exact string matching misses
75
+ * them. This mirrors the normalized + fuzzy matching the backend uses in
76
+ * agent/cloudflare/src/auto-fix-post-synthesis.ts (normalizeText / categoriesCompatible).
77
+ */
78
+ function normalizeText(value) {
79
+ return value.toLowerCase().replace(/\s+/g, ' ').trim();
80
+ }
81
+ /** Categories match if equal, or if one's tokens are a subset of the other's. */
82
+ function categoriesCompatible(left, right) {
83
+ const a = normalizeText(left);
84
+ const b = normalizeText(right);
85
+ if (a === b)
86
+ return true;
87
+ const aTokens = a.split(/[^a-z0-9]+/).filter(Boolean);
88
+ const bTokens = b.split(/[^a-z0-9]+/).filter(Boolean);
89
+ const aSet = new Set(aTokens);
90
+ const bSet = new Set(bTokens);
91
+ return ((bTokens.length > 0 && bTokens.every((t) => aSet.has(t))) ||
92
+ (aTokens.length > 0 && aTokens.every((t) => bSet.has(t))));
93
+ }
94
+ /** True if the finding matches any item the auto-fixer is handling. */
95
+ function isAutoFixing(finding, manifest) {
96
+ const items = manifest?.fixedItems ?? [];
97
+ if (items.length === 0)
98
+ return false;
99
+ const summaryKey = normalizeText(finding.summary);
100
+ if (summaryKey.length === 0)
101
+ return false;
102
+ return items.some((item) => normalizeText(item.summary) === summaryKey &&
103
+ categoriesCompatible(finding.category, item.category));
104
+ }
71
105
  function printRichOutput(pr, synthesis, manifest) {
72
106
  const reviewUrl = `https://haystackeditor.com/review/${pr.owner}/${pr.repo}/${pr.prNumber}`;
73
107
  console.log(`\n ${chalk.bold(`Haystack Triage: ${pr.owner}/${pr.repo}#${pr.prNumber}`)}\n`);
74
108
  // Auto-fixer status — what's being fixed vs what it skipped
75
109
  if (manifest && (manifest.fixedItems.length > 0 || manifest.leftForReview.length > 0)) {
76
110
  if (manifest.fixedItems.length > 0) {
77
- console.log(chalk.bold(' Auto-fixer is handling:\n'));
111
+ console.log(chalk.bold(' Auto-fixer is handling these (leave them alone):\n'));
78
112
  for (const item of manifest.fixedItems) {
79
113
  console.log(` - [${item.category}] ${item.summary}`);
80
114
  }
81
- console.log('');
115
+ console.log(` ${chalk.dim("Don't fix these. The auto-fixer is already on them and will push its own commit.")}\n`);
82
116
  }
83
117
  if (manifest.leftForReview.length > 0) {
84
- console.log(chalk.bold(' Auto-fixer skipped:\n'));
118
+ console.log(chalk.bold(' Auto-fixer skipped these (they need you):\n'));
85
119
  for (const item of manifest.leftForReview) {
86
120
  console.log(` - [${item.category}] ${item.summary}`);
87
121
  console.log(` ${chalk.dim(item.reason)}`);
@@ -89,10 +123,13 @@ function printRichOutput(pr, synthesis, manifest) {
89
123
  console.log('');
90
124
  }
91
125
  }
92
- // Findings — the actionable output for agents
93
- const findings = synthesis.synthesisDisplay || [];
126
+ // Findings — the actionable output for agents. Exclude anything the auto-fixer
127
+ // is already handling so the agent doesn't duplicate (and fight) its work.
128
+ const allFindings = synthesis.synthesisDisplay || [];
129
+ const findings = allFindings.filter((f) => !isAutoFixing(f, manifest));
130
+ const fixingCount = manifest?.fixedItems?.length ?? 0;
94
131
  if (findings.length > 0) {
95
- console.log(chalk.bold(' Findings:\n'));
132
+ console.log(chalk.bold(' Findings to address:\n'));
96
133
  for (let i = 0; i < findings.length; i++) {
97
134
  const finding = findings[i];
98
135
  const sourceTag = finding.source ? ` (${finding.source})` : '';
@@ -112,7 +149,11 @@ function printRichOutput(pr, synthesis, manifest) {
112
149
  console.log('');
113
150
  }
114
151
  }
115
- else if (!manifest || manifest.fixedItems.length === 0) {
152
+ else if (fixingCount > 0 && allFindings.length > 0) {
153
+ // Every finding is being auto-fixed — nothing left for the agent.
154
+ console.log(chalk.dim(' Nothing to address. Every finding is being auto-fixed (see above).\n'));
155
+ }
156
+ else {
116
157
  console.log(chalk.dim(' No findings.\n'));
117
158
  }
118
159
  console.log(` ${chalk.dim(reviewUrl)}\n`);
@@ -123,6 +164,7 @@ function printJsonOutput(pr, synthesis, manifest) {
123
164
  repo: pr.repo,
124
165
  prNumber: pr.prNumber,
125
166
  rating: synthesis.haystackRating,
167
+ // Issues the auto-fixer is already on — leave these alone, do not re-fix.
126
168
  autoFixerHandling: manifest?.fixedItems ?? [],
127
169
  autoFixerSkipped: manifest?.leftForReview ?? [],
128
170
  findings: (synthesis.synthesisDisplay || []).map((f) => ({
@@ -131,6 +173,8 @@ function printJsonOutput(pr, synthesis, manifest) {
131
173
  detail: f.detail,
132
174
  agentFixPrompt: f.agentFixPrompt || null,
133
175
  source: f.source || null,
176
+ // True when the auto-fixer is handling this finding — don't fix it yourself.
177
+ autoFixing: isAutoFixing(f, manifest),
134
178
  })),
135
179
  };
136
180
  console.log(JSON.stringify(output, null, 2));
@@ -159,16 +203,19 @@ function printHookOutput(pr, synthesis, manifest, status) {
159
203
  console.log(`[Haystack] ${label}: Analysis in progress...`);
160
204
  return;
161
205
  }
162
- const findings = synthesis.synthesisDisplay || [];
163
206
  const stars = ratingStars(synthesis.haystackRating);
164
207
  const fixingCount = manifest?.fixedItems?.length ?? 0;
165
- if (findings.length === 0 && fixingCount === 0) {
208
+ // Only count findings the auto-fixer is NOT handling \u2014 those are what need the user.
209
+ const actionable = (synthesis.synthesisDisplay || []).filter((f) => !isAutoFixing(f, manifest));
210
+ if (actionable.length === 0 && fixingCount === 0) {
166
211
  console.log(`[Haystack] \u2705 ${label} ${stars}: Good to merge`);
167
212
  }
213
+ else if (actionable.length === 0) {
214
+ // Everything is being auto-fixed \u2014 nothing for the user to do.
215
+ console.log(`[Haystack] \u2705 ${label} ${stars}: ${fixingCount} being auto-fixed, nothing for you to do`);
216
+ }
168
217
  else {
169
- const parts = [];
170
- if (findings.length > 0)
171
- parts.push(`${findings.length} finding(s)`);
218
+ const parts = [`${actionable.length} finding(s)`];
172
219
  if (fixingCount > 0)
173
220
  parts.push(`${fixingCount} being auto-fixed`);
174
221
  console.log(`[Haystack] \u26A0\uFE0F ${label} ${stars}: Needs your input (${parts.join(', ')})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.6",
3
+ "version": "0.15.7",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {