@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.
- package/dist/commands/setup.js +16 -0
- package/dist/commands/triage.js +59 -12
- package/package.json +1 -1
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
// =============================================================================
|
package/dist/commands/triage.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(', ')})`);
|