@haystackeditor/cli 0.12.3 → 0.13.1
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/README.md +1 -1
- package/dist/commands/config.js +7 -5
- package/dist/commands/dismiss.js +16 -10
- package/dist/commands/login.d.ts +4 -5
- package/dist/commands/login.js +95 -71
- package/dist/commands/pr-status.js +8 -4
- package/dist/commands/request-review.js +10 -8
- package/dist/commands/setup.js +8 -4
- package/dist/commands/submit.d.ts +3 -0
- package/dist/commands/submit.js +182 -25
- package/dist/commands/triage.js +12 -3
- package/dist/index.js +42 -5
- package/dist/triage/prompts.d.ts +3 -3
- package/dist/triage/prompts.js +24 -6
- package/dist/triage/runner.d.ts +7 -1
- package/dist/triage/runner.js +86 -21
- package/dist/triage/types.d.ts +3 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.js +12 -0
- package/dist/utils/auth.d.ts +50 -0
- package/dist/utils/auth.js +386 -0
- package/dist/utils/github-api.d.ts +36 -15
- package/dist/utils/github-api.js +52 -44
- package/package.json +1 -1
package/dist/commands/submit.js
CHANGED
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
import chalk from 'chalk';
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import { findGitRoot, getCurrentBranch, getDefaultBranch, hasUncommittedChanges, parseRemoteUrl, pushBranch, getLastCommitMessage, getCommitMessagesSinceBase, } from '../utils/git.js';
|
|
15
|
-
import { createPullRequest, findExistingPR,
|
|
15
|
+
import { createPullRequest, findExistingPR, getRepositoryAccess, requestReviewers, } from '../utils/github-api.js';
|
|
16
16
|
import { runTriage } from '../triage/runner.js';
|
|
17
|
+
import { loadConfig } from '../utils/config.js';
|
|
17
18
|
import { checkAnalysisReady, fetchAnalysisResults } from '../utils/analysis-api.js';
|
|
18
19
|
import { savePendingSubmit, updatePendingSubmit } from '../utils/pending-state.js';
|
|
19
20
|
import { ensureHaystackLabels, addLabelsToIssue, } from '../utils/github-api.js';
|
|
21
|
+
import { listStoredAccounts, resolveAuthContext, setRepoDefaultAccount, } from '../utils/auth.js';
|
|
20
22
|
// ============================================================================
|
|
21
23
|
// Helpers
|
|
22
24
|
// ============================================================================
|
|
@@ -27,6 +29,87 @@ function buildPRBody(commitMessages) {
|
|
|
27
29
|
---
|
|
28
30
|
*Created via [Haystack CLI](https://haystackeditor.com)*`;
|
|
29
31
|
}
|
|
32
|
+
function canCreateSameRepoPullRequest(permissions) {
|
|
33
|
+
return permissions?.admin === true
|
|
34
|
+
|| permissions?.maintain === true
|
|
35
|
+
|| permissions?.push === true;
|
|
36
|
+
}
|
|
37
|
+
function buildSubmitPermissionMessage(activeLogin, owner, repo, detail) {
|
|
38
|
+
const repoName = `${owner}/${repo}`;
|
|
39
|
+
const lines = [
|
|
40
|
+
`Haystack is authenticated as ${activeLogin}, but that account cannot create a same-repo pull request in ${repoName}.`,
|
|
41
|
+
'Use `haystack submit --account <login>` or `haystack auth use <login>` to select a different saved account.',
|
|
42
|
+
];
|
|
43
|
+
if (detail) {
|
|
44
|
+
lines.push(detail);
|
|
45
|
+
}
|
|
46
|
+
lines.push(`If the correct account is not saved yet, run \`haystack login\` with a GitHub account that has collaborator access to ${repoName}, then retry \`haystack submit\`.`);
|
|
47
|
+
return lines.join('\n');
|
|
48
|
+
}
|
|
49
|
+
function formatSubmitPermissionError(error, activeLogin, owner, repo) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
const lowerMessage = message.toLowerCase();
|
|
52
|
+
const shouldWrap = lowerMessage.includes('must be a collaborator')
|
|
53
|
+
|| lowerMessage.includes('permission denied')
|
|
54
|
+
|| message.startsWith('Not found: getting repository access');
|
|
55
|
+
if (!shouldWrap) {
|
|
56
|
+
return message;
|
|
57
|
+
}
|
|
58
|
+
return buildSubmitPermissionMessage(activeLogin, owner, repo, `GitHub said: ${message}`);
|
|
59
|
+
}
|
|
60
|
+
async function checkSubmitAccess(owner, repo, authContext) {
|
|
61
|
+
try {
|
|
62
|
+
const repoAccess = await getRepositoryAccess(owner, repo, authContext.token);
|
|
63
|
+
return { repoAccess };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { error };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function ambiguousAccountMessage(owner, repo, logins) {
|
|
70
|
+
return [
|
|
71
|
+
`Multiple saved Haystack accounts can create a same-repo pull request in ${owner}/${repo}: ${logins.join(', ')}.`,
|
|
72
|
+
'Re-run `haystack submit --account <login>` or `haystack auth use <login>` to choose one.',
|
|
73
|
+
].join('\n');
|
|
74
|
+
}
|
|
75
|
+
async function resolveSubmitAuthContext(owner, repo, preferredLogin) {
|
|
76
|
+
const primaryContext = await resolveAuthContext({ preferredLogin, owner, repo });
|
|
77
|
+
const primaryResult = await checkSubmitAccess(owner, repo, primaryContext);
|
|
78
|
+
if (primaryResult.repoAccess && canCreateSameRepoPullRequest(primaryResult.repoAccess.permissions)) {
|
|
79
|
+
return {
|
|
80
|
+
authContext: primaryContext,
|
|
81
|
+
repoAccess: primaryResult.repoAccess,
|
|
82
|
+
autoSelected: false,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (!preferredLogin) {
|
|
86
|
+
const alternatives = [];
|
|
87
|
+
const accounts = await listStoredAccounts();
|
|
88
|
+
for (const account of accounts) {
|
|
89
|
+
if (account.login === primaryContext.login)
|
|
90
|
+
continue;
|
|
91
|
+
const candidateContext = await resolveAuthContext({ preferredLogin: account.login });
|
|
92
|
+
const candidateResult = await checkSubmitAccess(owner, repo, candidateContext);
|
|
93
|
+
if (candidateResult.repoAccess && canCreateSameRepoPullRequest(candidateResult.repoAccess.permissions)) {
|
|
94
|
+
alternatives.push({
|
|
95
|
+
authContext: candidateContext,
|
|
96
|
+
repoAccess: candidateResult.repoAccess,
|
|
97
|
+
autoSelected: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (alternatives.length === 1) {
|
|
102
|
+
return alternatives[0];
|
|
103
|
+
}
|
|
104
|
+
if (alternatives.length > 1) {
|
|
105
|
+
throw new Error(ambiguousAccountMessage(owner, repo, alternatives.map((candidate) => candidate.authContext.login)));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (primaryResult.error) {
|
|
109
|
+
throw new Error(formatSubmitPermissionError(primaryResult.error, primaryContext.login, owner, repo));
|
|
110
|
+
}
|
|
111
|
+
throw new Error(buildSubmitPermissionMessage(primaryContext.login, owner, repo, 'The selected Haystack account does not have the collaborator access required for same-repo PR creation.'));
|
|
112
|
+
}
|
|
30
113
|
// ============================================================================
|
|
31
114
|
// Triage output
|
|
32
115
|
// ============================================================================
|
|
@@ -54,6 +137,47 @@ function checkerDisplayName(checker) {
|
|
|
54
137
|
default: return checker;
|
|
55
138
|
}
|
|
56
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Merge triage-tuning knobs from two sources, in ascending precedence:
|
|
142
|
+
* defaults (in runner.ts) < .haystack.json triage.* < CLI flags
|
|
143
|
+
*
|
|
144
|
+
* --max-turns <N> applies the same number to every checker. Per-checker
|
|
145
|
+
* granularity stays in .haystack.json (less common, more verbose).
|
|
146
|
+
*/
|
|
147
|
+
async function resolveTriageOptions(options) {
|
|
148
|
+
let config = null;
|
|
149
|
+
try {
|
|
150
|
+
config = await loadConfig();
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
// loadConfig returns null when the file doesn't exist — it only throws on
|
|
154
|
+
// read/parse failures, which we must surface so a malformed config doesn't
|
|
155
|
+
// silently lose the user's project-level triage overrides.
|
|
156
|
+
console.warn(chalk.yellow(`⚠ Could not load .haystack.json: ${err.message}`));
|
|
157
|
+
console.warn(chalk.dim(' Triage will use CLI flags / defaults — fix the config to apply project overrides.'));
|
|
158
|
+
}
|
|
159
|
+
const triageConfig = config?.triage ?? {};
|
|
160
|
+
const maxTurns = { ...(triageConfig.maxTurns ?? {}) };
|
|
161
|
+
if (typeof options.maxTurns === 'number' && options.maxTurns > 0) {
|
|
162
|
+
for (const name of ['code-review', 'rules-validator', 'intent-drift']) {
|
|
163
|
+
maxTurns[name] = options.maxTurns;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// loadConfig does a raw `as HaystackConfig` cast (no Zod runtime parse), so
|
|
167
|
+
// malformed .haystack.json values can reach us as strings/negatives/NaN.
|
|
168
|
+
// Mirror the positive-number guard we already apply to maxTurns before
|
|
169
|
+
// letting this flow into setTimeout.
|
|
170
|
+
const configTimeoutMs = typeof triageConfig.timeoutMs === 'number' && triageConfig.timeoutMs > 0
|
|
171
|
+
? triageConfig.timeoutMs
|
|
172
|
+
: undefined;
|
|
173
|
+
const timeoutMs = typeof options.triageTimeout === 'number' && options.triageTimeout > 0
|
|
174
|
+
? options.triageTimeout * 1000
|
|
175
|
+
: configTimeoutMs;
|
|
176
|
+
return {
|
|
177
|
+
maxTurns: Object.keys(maxTurns).length > 0 ? maxTurns : undefined,
|
|
178
|
+
timeoutMs,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
57
181
|
function isCodexNetworkDisabledTriageError(err) {
|
|
58
182
|
const message = err instanceof Error ? err.message : String(err);
|
|
59
183
|
return message.includes('CODEX_SANDBOX_NETWORK_DISABLED=1');
|
|
@@ -80,11 +204,19 @@ function printTriageSummary(result) {
|
|
|
80
204
|
console.log('');
|
|
81
205
|
}
|
|
82
206
|
}
|
|
83
|
-
//
|
|
207
|
+
// Hard errors from sub-agent failures (auth, crashes) — these block submit.
|
|
84
208
|
if (result.errors.length > 0) {
|
|
85
|
-
console.log(chalk.
|
|
209
|
+
console.log(chalk.red(' Checker errors:'));
|
|
86
210
|
for (const err of result.errors) {
|
|
87
|
-
console.log(chalk.
|
|
211
|
+
console.log(chalk.red(` ✗ ${err}`));
|
|
212
|
+
}
|
|
213
|
+
console.log('');
|
|
214
|
+
}
|
|
215
|
+
// Soft failures (max turns, timeout) — non-blocking, purely informational.
|
|
216
|
+
if (result.warnings.length > 0) {
|
|
217
|
+
console.log(chalk.yellow(' Checker warnings (non-blocking):'));
|
|
218
|
+
for (const warn of result.warnings) {
|
|
219
|
+
console.log(chalk.yellow(` ! ${warn}`));
|
|
88
220
|
}
|
|
89
221
|
console.log('');
|
|
90
222
|
}
|
|
@@ -93,9 +225,14 @@ function printTriageSummary(result) {
|
|
|
93
225
|
const errorCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'error').length, 0);
|
|
94
226
|
const warningCount = result.results.reduce((sum, r) => sum + r.issues.filter((i) => i.severity === 'warning').length, 0);
|
|
95
227
|
const duration = (result.duration / 1000).toFixed(1);
|
|
96
|
-
if (result.passed) {
|
|
228
|
+
if (result.passed && result.results.length > 0) {
|
|
97
229
|
console.log(chalk.green(` All checks passed (${duration}s)\n`));
|
|
98
230
|
}
|
|
231
|
+
else if (result.passed) {
|
|
232
|
+
// No blocking issues, but nothing actually completed either — the
|
|
233
|
+
// triage-unavailable warning will print separately.
|
|
234
|
+
console.log(chalk.yellow(` No checkers produced results (${duration}s)\n`));
|
|
235
|
+
}
|
|
99
236
|
else {
|
|
100
237
|
const parts = [];
|
|
101
238
|
if (errorCount > 0)
|
|
@@ -127,21 +264,34 @@ export async function submitCommand(options) {
|
|
|
127
264
|
console.error(chalk.red('\nNot a git repository.\n'));
|
|
128
265
|
process.exit(1);
|
|
129
266
|
}
|
|
130
|
-
//
|
|
131
|
-
let
|
|
267
|
+
// Get remote info
|
|
268
|
+
let remoteInfo;
|
|
132
269
|
try {
|
|
133
|
-
|
|
134
|
-
console.log(chalk.green(
|
|
270
|
+
remoteInfo = parseRemoteUrl('origin');
|
|
271
|
+
console.log(chalk.green(`✓ Repository: ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
135
272
|
}
|
|
136
273
|
catch (err) {
|
|
137
274
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
138
275
|
process.exit(1);
|
|
139
276
|
}
|
|
140
|
-
|
|
141
|
-
let
|
|
277
|
+
let githubToken;
|
|
278
|
+
let activeGitHubLogin;
|
|
279
|
+
let autoSelectedAccount = false;
|
|
142
280
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
281
|
+
const authResolution = await resolveSubmitAuthContext(remoteInfo.owner, remoteInfo.repo, options.account);
|
|
282
|
+
githubToken = authResolution.authContext.token;
|
|
283
|
+
activeGitHubLogin = authResolution.authContext.login;
|
|
284
|
+
autoSelectedAccount = authResolution.autoSelected;
|
|
285
|
+
try {
|
|
286
|
+
await setRepoDefaultAccount(remoteInfo.owner, remoteInfo.repo, activeGitHubLogin);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
console.log(chalk.yellow(`⚠ Could not save ${activeGitHubLogin} as the default Haystack account for ${remoteInfo.owner}/${remoteInfo.repo}: ${err.message}`));
|
|
290
|
+
}
|
|
291
|
+
console.log(chalk.green(`✓ Authenticated as ${activeGitHubLogin}`));
|
|
292
|
+
if (autoSelectedAccount) {
|
|
293
|
+
console.log(chalk.dim(` Auto-selected saved account ${activeGitHubLogin} for ${remoteInfo.owner}/${remoteInfo.repo}`));
|
|
294
|
+
}
|
|
145
295
|
}
|
|
146
296
|
catch (err) {
|
|
147
297
|
console.error(chalk.red(`\n${err.message}\n`));
|
|
@@ -162,11 +312,17 @@ export async function submitCommand(options) {
|
|
|
162
312
|
if (!options.force) {
|
|
163
313
|
console.log(chalk.dim('\nRunning pre-PR triage checks...'));
|
|
164
314
|
try {
|
|
165
|
-
const
|
|
315
|
+
const triageOptions = await resolveTriageOptions(options);
|
|
316
|
+
const triageResult = await runTriage(gitRoot, baseBranch, triageOptions);
|
|
166
317
|
printTriageSummary(triageResult);
|
|
167
|
-
|
|
318
|
+
// No valid results means we got zero signal from triage. That's
|
|
319
|
+
// "unavailable" whether the cause was a hard error (auth crash) or a
|
|
320
|
+
// soft cap (every checker hit max turns). Either way don't pretend it
|
|
321
|
+
// passed — print the warning and continue.
|
|
322
|
+
const triageInfraFailure = triageResult.results.length === 0 &&
|
|
323
|
+
(triageResult.errors.length > 0 || triageResult.warnings.length > 0);
|
|
168
324
|
if (triageInfraFailure) {
|
|
169
|
-
console.log(chalk.yellow('\n⚠ Triage unavailable (
|
|
325
|
+
console.log(chalk.yellow('\n⚠ Triage unavailable (no checkers produced results). Continuing with submit.\n'));
|
|
170
326
|
}
|
|
171
327
|
if (!triageResult.passed) {
|
|
172
328
|
if (triageInfraFailure) {
|
|
@@ -219,7 +375,7 @@ export async function submitCommand(options) {
|
|
|
219
375
|
// -------------------------------------------------------------------------
|
|
220
376
|
console.log(chalk.dim('\nCreating pull request...'));
|
|
221
377
|
// Check if PR already exists
|
|
222
|
-
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch);
|
|
378
|
+
const existingPR = await findExistingPR(remoteInfo.owner, remoteInfo.repo, currentBranch, githubToken);
|
|
223
379
|
let prNumber;
|
|
224
380
|
let prUrl;
|
|
225
381
|
if (existingPR) {
|
|
@@ -255,13 +411,14 @@ export async function submitCommand(options) {
|
|
|
255
411
|
base: baseBranch,
|
|
256
412
|
body: prBody,
|
|
257
413
|
draft: options.draft,
|
|
414
|
+
token: githubToken,
|
|
258
415
|
});
|
|
259
416
|
prNumber = pr.number;
|
|
260
417
|
prUrl = pr.html_url;
|
|
261
418
|
console.log(chalk.green(`✓ Pull request created: #${prNumber}`));
|
|
262
419
|
}
|
|
263
420
|
catch (err) {
|
|
264
|
-
console.error(chalk.red(`\n${err.
|
|
421
|
+
console.error(chalk.red(`\n${formatSubmitPermissionError(err, activeGitHubLogin, remoteInfo.owner, remoteInfo.repo)}\n`));
|
|
265
422
|
process.exit(1);
|
|
266
423
|
}
|
|
267
424
|
}
|
|
@@ -270,8 +427,8 @@ export async function submitCommand(options) {
|
|
|
270
427
|
// -------------------------------------------------------------------------
|
|
271
428
|
if (options.autoMerge) {
|
|
272
429
|
try {
|
|
273
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge']);
|
|
274
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge']);
|
|
430
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-merge'], githubToken);
|
|
431
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-merge'], githubToken);
|
|
275
432
|
console.log(chalk.green('✓ Auto-merge enabled for this PR'));
|
|
276
433
|
}
|
|
277
434
|
catch (err) {
|
|
@@ -283,8 +440,8 @@ export async function submitCommand(options) {
|
|
|
283
440
|
// -------------------------------------------------------------------------
|
|
284
441
|
if (options.autoFix) {
|
|
285
442
|
try {
|
|
286
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix']);
|
|
287
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix']);
|
|
443
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:auto-fix'], githubToken);
|
|
444
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:auto-fix'], githubToken);
|
|
288
445
|
console.log(chalk.green('✓ Auto-fix (alpha) enabled for this PR'));
|
|
289
446
|
}
|
|
290
447
|
catch (err) {
|
|
@@ -296,11 +453,11 @@ export async function submitCommand(options) {
|
|
|
296
453
|
// -------------------------------------------------------------------------
|
|
297
454
|
if (options.review) {
|
|
298
455
|
try {
|
|
299
|
-
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review']);
|
|
300
|
-
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review']);
|
|
456
|
+
await ensureHaystackLabels(remoteInfo.owner, remoteInfo.repo, ['haystack:needs-review'], githubToken);
|
|
457
|
+
await addLabelsToIssue(remoteInfo.owner, remoteInfo.repo, prNumber, ['haystack:needs-review'], githubToken);
|
|
301
458
|
if (typeof options.review === 'string') {
|
|
302
459
|
// Specific reviewer requested
|
|
303
|
-
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review]);
|
|
460
|
+
await requestReviewers(remoteInfo.owner, remoteInfo.repo, prNumber, [options.review], githubToken);
|
|
304
461
|
console.log(chalk.green(`✓ Review requested from ${options.review}`));
|
|
305
462
|
}
|
|
306
463
|
else {
|
package/dist/commands/triage.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import chalk from 'chalk';
|
|
23
23
|
import { checkAnalysisReady, fetchRatingSynthesis, fetchAutoFixManifest } from '../utils/analysis-api.js';
|
|
24
24
|
import { parseRemoteUrl } from '../utils/git.js';
|
|
25
|
-
import { loadToken } from '
|
|
25
|
+
import { loadToken } from '../utils/auth.js';
|
|
26
26
|
import { loadPendingSubmit, updatePendingSubmit, clearPendingSubmit } from '../utils/pending-state.js';
|
|
27
27
|
// ============================================================================
|
|
28
28
|
// PR identifier parsing
|
|
@@ -248,9 +248,18 @@ export async function triageCommand(identifier, options) {
|
|
|
248
248
|
const pr = resolvePR(identifier, options);
|
|
249
249
|
if (!pr)
|
|
250
250
|
return;
|
|
251
|
-
// Load GitHub token for private repo auth
|
|
252
|
-
const token = await loadToken() || undefined;
|
|
253
251
|
const isQuiet = options.hook || options.json;
|
|
252
|
+
// Load GitHub token for private repo auth
|
|
253
|
+
let token;
|
|
254
|
+
try {
|
|
255
|
+
token = await loadToken({ owner: pr.owner, repo: pr.repo }) || undefined;
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
if (!isQuiet) {
|
|
259
|
+
console.log(chalk.yellow(`\n⚠ Could not load Haystack credentials: ${err.message}`));
|
|
260
|
+
console.log(chalk.dim(' Continuing without GitHub auth. Private repos may require `haystack login`.\n'));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
254
263
|
if (!isQuiet) {
|
|
255
264
|
console.log(chalk.dim(`\nFetching analysis for ${pr.owner}/${pr.repo}#${pr.prNumber}...`));
|
|
256
265
|
}
|
package/dist/index.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { Command } from 'commander';
|
|
23
23
|
import { statusCommand } from './commands/status.js';
|
|
24
24
|
import { initCommand } from './commands/init.js';
|
|
25
|
-
import { loginCommand, logoutCommand } from './commands/login.js';
|
|
25
|
+
import { authListCommand, authUseCommand, loginCommand, logoutCommand, whoamiCommand, } from './commands/login.js';
|
|
26
26
|
import { handleAgenticTool, handleAutoMerge, handleWaitForReviewers, isAutoMergeEnabled } from './commands/config.js';
|
|
27
27
|
import { installSkills, listSkills } from './commands/skills.js';
|
|
28
28
|
import { hooksInstall, hooksStatus, hooksUpdate } from './commands/hooks.js';
|
|
@@ -38,7 +38,7 @@ const program = new Command();
|
|
|
38
38
|
program
|
|
39
39
|
.name('haystack')
|
|
40
40
|
.description('Haystack CLI — automated PR review, triage, and merge queue')
|
|
41
|
-
.version('0.
|
|
41
|
+
.version('0.13.1');
|
|
42
42
|
program
|
|
43
43
|
.command('init')
|
|
44
44
|
.description('Create .haystack.json configuration')
|
|
@@ -78,15 +78,31 @@ program
|
|
|
78
78
|
.action(statusCommand);
|
|
79
79
|
program
|
|
80
80
|
.command('login')
|
|
81
|
-
.description('
|
|
81
|
+
.description('Add or refresh a saved GitHub account')
|
|
82
82
|
.action(loginCommand);
|
|
83
83
|
program
|
|
84
|
-
.command('logout')
|
|
85
|
-
.description('Remove
|
|
84
|
+
.command('logout [login]')
|
|
85
|
+
.description('Remove a saved GitHub account (defaults to the active account)')
|
|
86
86
|
.action(logoutCommand);
|
|
87
|
+
program
|
|
88
|
+
.command('whoami')
|
|
89
|
+
.description('Show the active Haystack GitHub account')
|
|
90
|
+
.action(whoamiCommand);
|
|
91
|
+
const authProgram = program
|
|
92
|
+
.command('auth')
|
|
93
|
+
.description('Manage saved Haystack GitHub accounts');
|
|
94
|
+
authProgram
|
|
95
|
+
.command('list')
|
|
96
|
+
.description('List saved Haystack GitHub accounts')
|
|
97
|
+
.action(authListCommand);
|
|
98
|
+
authProgram
|
|
99
|
+
.command('use <login>')
|
|
100
|
+
.description('Set the active Haystack GitHub account')
|
|
101
|
+
.action(authUseCommand);
|
|
87
102
|
program
|
|
88
103
|
.command('submit')
|
|
89
104
|
.description('Create a PR from current changes')
|
|
105
|
+
.option('--account <login>', 'Saved Haystack account login to use for GitHub API calls')
|
|
90
106
|
.option('--title <title>', 'PR title (default: last commit message)')
|
|
91
107
|
.option('--body <body>', 'PR body (default: commit messages since base)')
|
|
92
108
|
.option('--body-file <path>', 'Read PR body from file (use "-" for stdin)')
|
|
@@ -97,6 +113,8 @@ program
|
|
|
97
113
|
.option('--auto-fix', 'Alpha auto-fix for straightforward mechanical issues (discouraged)')
|
|
98
114
|
.option('--auto-merge', 'Apply auto-merge label (default: from .haystack.json, --no-auto-merge to disable)')
|
|
99
115
|
.option('--no-wait', 'Skip waiting for analysis results')
|
|
116
|
+
.option('--max-turns <n>', 'Max agentic turns per triage checker (overrides .haystack.json triage.maxTurns)', (v) => parseInt(v, 10))
|
|
117
|
+
.option('--triage-timeout <seconds>', 'Wall-clock timeout per triage checker in seconds (overrides .haystack.json triage.timeoutMs)', (v) => parseInt(v, 10))
|
|
100
118
|
.addHelpText('after', `
|
|
101
119
|
This command is designed for AI coding agents to submit PRs.
|
|
102
120
|
|
|
@@ -116,6 +134,22 @@ Pre-PR Triage:
|
|
|
116
134
|
Use --auto-fix only when explicitly opting into the alpha fixer for
|
|
117
135
|
straightforward mechanical issues.
|
|
118
136
|
|
|
137
|
+
Triage budgets:
|
|
138
|
+
• --max-turns <n> Raise/lower the per-checker tool-use turn cap.
|
|
139
|
+
Defaults: code-review 8, rules-validator 10,
|
|
140
|
+
intent-drift 10. Applies the same N to all three.
|
|
141
|
+
• --triage-timeout <sec> Raise/lower the per-checker wall-clock timeout
|
|
142
|
+
(default: 180s).
|
|
143
|
+
|
|
144
|
+
Persist these in .haystack.json to apply per project:
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
"triage": {
|
|
148
|
+
"maxTurns": { "code-review": 12 },
|
|
149
|
+
"timeoutMs": 240000
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
119
153
|
Review Routing:
|
|
120
154
|
If repo auto-merge is enabled in .haystack.json, plain \`haystack submit\`
|
|
121
155
|
enters the auto-merge queue after submission. Otherwise it still runs the
|
|
@@ -146,6 +180,9 @@ Examples:
|
|
|
146
180
|
haystack submit --draft # Create as draft PR
|
|
147
181
|
haystack submit --review # ⚠ Blocks auto-merge, needs human approval
|
|
148
182
|
haystack submit --review octocat # ⚠ Blocks auto-merge, requests review from octocat
|
|
183
|
+
haystack submit --account octocat # Use a specific saved Haystack account for this submit
|
|
184
|
+
haystack submit --max-turns 12 # Raise triage turn cap to 12 per checker
|
|
185
|
+
haystack submit --triage-timeout 300 # Raise triage wall-clock to 5 minutes
|
|
149
186
|
`)
|
|
150
187
|
.action(async (options) => {
|
|
151
188
|
// Resolve --auto-merge default from .haystack.json when not explicitly set
|
package/dist/triage/prompts.d.ts
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* Build the code review prompt.
|
|
12
12
|
* Always runs — looks for objective bugs in the diff.
|
|
13
13
|
*/
|
|
14
|
-
export declare function buildCodeReviewPrompt(baseBranch: string, outputPath: string, precomputedDiff?: string | null): string;
|
|
14
|
+
export declare function buildCodeReviewPrompt(baseBranch: string, outputPath: string, maxTurns: number, timeoutMs: number, precomputedDiff?: string | null): string;
|
|
15
15
|
/**
|
|
16
16
|
* Build the rules validator prompt.
|
|
17
17
|
* Runs if .haystack/pr-rules.yml exists OR agent instruction files are found.
|
|
18
18
|
*
|
|
19
19
|
* @returns The prompt string, or null if no rules content or agent policies provided.
|
|
20
20
|
*/
|
|
21
|
-
export declare function buildRulesValidatorPrompt(baseBranch: string, rulesYaml: string, outputPath: string, agentPolicies?: {
|
|
21
|
+
export declare function buildRulesValidatorPrompt(baseBranch: string, rulesYaml: string, outputPath: string, maxTurns: number, timeoutMs: number, agentPolicies?: {
|
|
22
22
|
filename: string;
|
|
23
23
|
content: string;
|
|
24
24
|
}[], precomputedDiff?: string | null): string | null;
|
|
@@ -28,4 +28,4 @@ export declare function buildRulesValidatorPrompt(baseBranch: string, rulesYaml:
|
|
|
28
28
|
*
|
|
29
29
|
* @returns The prompt string, or null if no trace files provided.
|
|
30
30
|
*/
|
|
31
|
-
export declare function buildIntentDriftPrompt(baseBranch: string, traceFiles: string[], outputPath: string, precomputedDiff?: string | null): string | null;
|
|
31
|
+
export declare function buildIntentDriftPrompt(baseBranch: string, traceFiles: string[], outputPath: string, maxTurns: number, timeoutMs: number, precomputedDiff?: string | null): string | null;
|
package/dist/triage/prompts.js
CHANGED
|
@@ -55,11 +55,29 @@ const INTENT_DRIFT_SCHEMA = `{
|
|
|
55
55
|
// ============================================================================
|
|
56
56
|
// Prompt builders
|
|
57
57
|
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Build the time-budget preamble shared by every checker prompt. Agents don't
|
|
60
|
+
* otherwise know about the externally-enforced turn cap and wall-clock
|
|
61
|
+
* deadline, so they'd happily burn budget on deep dives and get SIGTERM'd.
|
|
62
|
+
* Telling them up front makes them triage instead of exhaustively explore.
|
|
63
|
+
*/
|
|
64
|
+
function buildTimeBudgetHeader(maxTurns, timeoutMs) {
|
|
65
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
66
|
+
return `## Time budget (hard limits)
|
|
67
|
+
|
|
68
|
+
- You have **${maxTurns} tool-use turns max** and **~${minutes} minutes wall-clock** before this process is killed.
|
|
69
|
+
- Be decisive. Skip speculative exploration. If a file looks incidental, don't open it.
|
|
70
|
+
- Prefer the provided diff over running fresh searches unless the diff alone is ambiguous.
|
|
71
|
+
- Report only findings you can confirm quickly. A timeout produces ZERO findings, so ship a partial high-confidence result rather than chase a perfect one that never lands.
|
|
72
|
+
- Write the output JSON file EARLY (even if partial) and update it if you find more. Never exit without writing.
|
|
73
|
+
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
58
76
|
/**
|
|
59
77
|
* Build the code review prompt.
|
|
60
78
|
* Always runs — looks for objective bugs in the diff.
|
|
61
79
|
*/
|
|
62
|
-
export function buildCodeReviewPrompt(baseBranch, outputPath, precomputedDiff) {
|
|
80
|
+
export function buildCodeReviewPrompt(baseBranch, outputPath, maxTurns, timeoutMs, precomputedDiff) {
|
|
63
81
|
const diffSection = precomputedDiff
|
|
64
82
|
? `## Diff (precomputed)
|
|
65
83
|
|
|
@@ -79,7 +97,7 @@ ${precomputedDiff}
|
|
|
79
97
|
3. Identify only REAL BUGS — things that will definitely crash, produce wrong results, or corrupt data.`;
|
|
80
98
|
return `You are a pre-PR code reviewer. Your job is to find OBJECTIVE BUGS in the code changes that will cause incorrect runtime behavior.
|
|
81
99
|
|
|
82
|
-
${diffSection}
|
|
100
|
+
${buildTimeBudgetHeader(maxTurns, timeoutMs)}${diffSection}
|
|
83
101
|
|
|
84
102
|
## What to flag
|
|
85
103
|
|
|
@@ -122,7 +140,7 @@ Be extremely conservative. False positives waste the developer's time. Only flag
|
|
|
122
140
|
*
|
|
123
141
|
* @returns The prompt string, or null if no rules content or agent policies provided.
|
|
124
142
|
*/
|
|
125
|
-
export function buildRulesValidatorPrompt(baseBranch, rulesYaml, outputPath, agentPolicies, precomputedDiff) {
|
|
143
|
+
export function buildRulesValidatorPrompt(baseBranch, rulesYaml, outputPath, maxTurns, timeoutMs, agentPolicies, precomputedDiff) {
|
|
126
144
|
const hasRules = rulesYaml.trim().length > 0;
|
|
127
145
|
const hasPolicies = agentPolicies && agentPolicies.length > 0;
|
|
128
146
|
if (!hasRules && !hasPolicies)
|
|
@@ -196,7 +214,7 @@ ${precomputedDiff}
|
|
|
196
214
|
3. If you need more context, read just the relevant section of the file — do NOT read entire large files.`;
|
|
197
215
|
return `You are a PR rules validator. Your job is to check the code changes against project rules and policies, then flag violations.
|
|
198
216
|
|
|
199
|
-
${rulesSection}${policiesSection}${diffInstructions}
|
|
217
|
+
${buildTimeBudgetHeader(maxTurns, timeoutMs)}${rulesSection}${policiesSection}${diffInstructions}
|
|
200
218
|
|
|
201
219
|
## Important
|
|
202
220
|
|
|
@@ -223,13 +241,13 @@ ${RULES_VALIDATOR_SCHEMA}
|
|
|
223
241
|
*
|
|
224
242
|
* @returns The prompt string, or null if no trace files provided.
|
|
225
243
|
*/
|
|
226
|
-
export function buildIntentDriftPrompt(baseBranch, traceFiles, outputPath, precomputedDiff) {
|
|
244
|
+
export function buildIntentDriftPrompt(baseBranch, traceFiles, outputPath, maxTurns, timeoutMs, precomputedDiff) {
|
|
227
245
|
if (traceFiles.length === 0)
|
|
228
246
|
return null;
|
|
229
247
|
const traceFileList = traceFiles.map(f => `- \`${f}\``).join('\n');
|
|
230
248
|
return `You are an intent drift detector. Your job is to check whether an AI coding agent faithfully implemented what the user asked for.
|
|
231
249
|
|
|
232
|
-
## Context
|
|
250
|
+
${buildTimeBudgetHeader(maxTurns, timeoutMs)}## Context
|
|
233
251
|
|
|
234
252
|
This PR was created by an AI coding agent. The agent's session transcripts (traces) are stored locally. You will compare what the user asked the agent to do against what was actually implemented in the diff.
|
|
235
253
|
|
package/dist/triage/runner.d.ts
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
* parallel sub-processes for each checker, and collects structured JSON results.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgenticCLI, TriageResult } from './types.js';
|
|
8
|
+
export interface RunTriageOptions {
|
|
9
|
+
/** Per-checker max-turn overrides (CLI flag > .haystack.json > defaults). */
|
|
10
|
+
maxTurns?: Partial<Record<string, number>>;
|
|
11
|
+
/** Per-checker wall-clock timeout override, in ms. */
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}
|
|
8
14
|
/**
|
|
9
15
|
* Detect which agentic CLI is installed.
|
|
10
16
|
* Returns the first one found, or null if none available.
|
|
@@ -18,4 +24,4 @@ export declare function detectAgenticCLI(): AgenticCLI | null;
|
|
|
18
24
|
* @returns Aggregated triage results
|
|
19
25
|
* @throws If no agentic CLI is detected
|
|
20
26
|
*/
|
|
21
|
-
export declare function runTriage(gitRoot: string, baseBranch: string): Promise<TriageResult>;
|
|
27
|
+
export declare function runTriage(gitRoot: string, baseBranch: string, options?: RunTriageOptions): Promise<TriageResult>;
|